Skip to content

JavaScript SDK (Client) Integration

The HashEntry CMP SDK is a dependency-free script that shows the cookie banner, blocks the scripts you tag until the visitor allows them, and records each decision on the HashEntry server. It can also record document approvals (terms, contracts, policies) — see Document Approval & Signature.

This page describes what the SDK does today. Where a behaviour depends on a dashboard setting or on your own markup, it says so.


<head>
<script
src="https://tool.hashentry.com/cmp.js"
data-site="YOUR_SITE_API_KEY"
async
></script>
</head>

The SDK loads the configuration bound to your API key (banner design, texts, categories, documents, behaviour settings) from the server and decides whether a banner is needed for this visitor.

Two things must be in place before a cookie decision can be recorded on the server:

  1. The site is verified in the dashboard. Requests for an unverified site are rejected with 403 SITE_NOT_VERIFIED.
  2. A cookie policy document is attached to the site. See the next section.

A cookie consent is recorded only if it references the exact active version of a document of type cookie_policy that is attached to the site. The accepted policy version is part of the evidence, so the SDK does not relax this rule.

  • If no published cookie policy is attached, the SDK does not show the banner (nor the widget or the “Do Not Sell” link) and keeps every tagged non-essential script blocked, in every region: a choice that cannot be recorded as evidence is not collected. The consent:error and error events fire with code: "COOKIE_POLICY_MISSING" and the error is written to the browser console. CMP.accept() / CMP.reject() resolve with success: false, error.code = "COOKIE_POLICY_MISSING" and pending_sync: false; nothing is stored, applied or queued. Once a policy is attached, the banner appears on the next page load and the visitor decides against that version. A consent that was recorded while a policy was attached stays valid.
  • A decision is never held back and sent later with a policy version the visitor has not seen. If an earlier SDK version left such a decision in the local queue, it is dropped (consent:error with final: true, reprompt: true) and the visitor is asked again.
  • The only exception is local mode without data-site: nothing is sent to the server, so the banner is shown and decisions stay in the browser (local_only: true).
  • If the consent references a version that is not the active one, the server answers 409 CONFIG_OUTDATED. See Re-consent and changed document versions.

What you must do: create a cookie policy document in the dashboard, publish a version in every language your banner uses, and attach it to the site.


Only scripts that you tag are blocked. The SDK does not scan the page for trackers and does not intercept scripts you leave untouched: a normal <script> tag runs immediately, with or without consent.

To hold a script back, change its type to text/plain and name its category:

<!-- Inline script: runs only after "analytics" is allowed -->
<script type="text/plain" data-cookie-category="analytics">
/* analytics snippet */
</script>
<!-- External script: use data-src instead of src -->
<script type="text/plain" data-cookie-category="marketing"
data-src="https://connect.facebook.net/en_US/fbevents.js"></script>
  • Category names must match the category keys of your banner (analytics, marketing, preferences, or your own keys). Scripts tagged necessary are activated immediately, before the configuration is loaded.
  • Tagged scripts that are added to the page later (SPA navigation, tag injection) are handled as well.
  • A script that has already run cannot be stopped. When a visitor revokes a category whose scripts ran on the current page, the SDK deletes the known cookies of that category and, by default, reloads the page (see reload_on_revoke).
  • When a category is revoked, the SDK deletes the cookies / storage entries listed for that category in your banner configuration, plus a small built-in list of well-known names. Cookies it does not know about are not deleted, and third-party cookies on other domains cannot be deleted from your page.

AttributeDefaultDescription
data-site—Site API key. Required in remote mode; also needed in the other modes for decisions to be recorded on the server.
data-modelocal-fallbackConfiguration source: remote, local, local-fallback. See Operating modes.
data-languageautoBanner language. auto uses <html lang>, then the browser language.
data-regionautoForces a region or framework (DE, US, gdpr, ccpa, none, …) instead of detection. This is the compliance region (which rules the banner follows); it does not choose where data is stored or which API host is called.
data-config—Inline JSON configuration (for local / local-fallback).
data-debugfalsetrue prints debug logs. Errors and warnings are always printed to the console.
data-headlessfalsetrue renders no UI at all (banner, widget, “Do Not Sell” link). See Headless mode.
data-consent-modedashboard settingtrue / false forces Google Consent Mode v2 on or off. See Google Consent Mode v2.
data-reload-on-revokedashboard settingtrue / false overrides the reload_on_revoke behaviour flag.
data-modules—Modules to load up front, e.g. documents, preferences. See Bundles and on-demand modules.
data-module-basescript’s own folderBase URL the module files are loaded from. Only needed when they are not served next to cmp.js.
data-api-urlHashEntry APIAPI base URL for a first-party proxy or custom domain. If your account’s data region has its own API host, the configuration response announces it (api_base_url) and the SDK uses it for all later calls; the embed code generated in the panel then already contains the matching data-api-url. A value you set here always takes precedence.

window.CMP exists as soon as the script has run. Methods that record something return a promise with a result object; they do not throw.

CMP.ready(() => { /* configuration loaded, consent state evaluated */ });
// Banner
CMP.show(); // open the banner (e.g. from a footer link)
CMP.hide();
// Decisions — each returns Promise<ConsentResult>
CMP.accept(); // allow all categories
CMP.accept({ marketing: false }); // allow all except marketing
CMP.reject(); // only necessary
CMP.withdraw(); // withdraw the stored consent
// State
CMP.getConsent(); // ConsentState or null
CMP.hasConsent('analytics'); // boolean ("necessary" is always true)
// Language
CMP.setLanguage('de');
CMP.getLanguage();
CMP.setLocale('de', { ccpa: { optOut: 'Widersprechen' } }); // texts for one language
// Metadata: added to every consent and approval recorded afterwards
CMP.setMetadata({ order_id: 'ORD-54321' });
CMP.setMetadata('customer_id', 'CUST-999');
CMP.getMetadata();
// Region override, theme, API URL, local configuration
CMP.setRegion('DE');
CMP.setTheme({ primaryColor: '#0F766E' });
CMP.setApiUrl('/hashentry-api');
CMP.getApiUrl();
CMP.setConfig({ banner_config: { /* … */ } });
// Events
CMP.on('consent:given', (payload) => {});
CMP.off('consent:given', handler);
// Documents (loaded on demand — see "Document Approval & Signature")
CMP.showDocumentApproval('terms-of-service', { language: 'en' });
CMP.approveDocument('terms-of-service', { /* … */ });

Withdrawing turns all non-essential categories off, deletes the known cookies of those categories, sets Google Consent Mode to denied, sends a decision of withdrawn to the server (if a consent had been stored), removes the stored consent from the browser and — in opt-in regions — shows the banner again. If scripts of a revoked category already ran on the page, the page is reloaded unless reload_on_revoke is off.

Changes the language of the open banner and reloads the configuration for that language, because document versions are language-specific: later consents then reference the cookie policy version of the new language. If the banner has no translation for the language, it falls back to the banner’s default language.

Values are merged and sent as metadata with every later cookie decision and document approval. The server accepts at most 30 keys and about 16 KB of metadata per record.

Calling the API before the script has loaded

Section titled “Calling the API before the script has loaded”
<script>
window.__CMP_QUEUE = window.__CMP_QUEUE || [];
window.__CMP_QUEUE.push(['setLanguage', 'de']);
window.__CMP_QUEUE.push(['setMetadata', { order_id: 'ORD-12345' }]);
window.__CMP_QUEUE.push(['on', 'consent:given', (p) => console.log(p)]);
</script>

Queued calls run in order when the SDK starts. A language set this way is not overridden by automatic detection.


{
success: true,
consent_token: 'ct_…',
proof_hash: '…', // 64 hex characters
created_at: '2026-10-10T10:00:00+00:00',
idempotency_key: '…' // one UUID per user action, reused on every retry
}

success is true only when the server has accepted the record. Otherwise:

{
success: false,
consent_token: '',
proof_hash: '',
created_at: '…',
error: { code: 'NETWORK_ERROR', status: 0, message: '…' },
pending_sync: true, // kept in the local queue, will be retried
idempotency_key: '…'
}
  • error.code is the server’s machine code or one of the SDK codes below; error.status is the HTTP status (0 when no response was received).
  • pending_sync: true means the decision is applied in the browser and waits in a local queue (localStorage). It is retried on the next page load, when the tab becomes visible again and when the browser comes back online, always with the same idempotency_key, for up to 7 days or 12 attempts.
  • local_only: true appears when there is nothing to send (for example local mode without data-site).

The visitor’s choice is applied in the browser immediately in every case; the result tells you whether the server has a record of it. The exception is COOKIE_POLICY_MISSING: the decision is neither stored nor applied.

{ success: true, decision: 'accepted', consent_token: 'ct_…', proof_hash: '…', slug: 'terms-of-service', version: '2.0.0' }
{ success: false, slug: 'terms-of-service', version: '2.0.0', error: { code: 'DOCUMENT_VERSION_CHANGED', status: 409, message: '…' } }

Document approvals are never queued for later: if success is not true, the approval has not been recorded.


EventPayloadWhen
ready{}Start-up finished (also when the configuration could not be loaded).
banner:shown{ type } or { headless: true, reason, config }The banner was shown, or — in headless mode — should be shown by you.
banner:hidden{ reason } or { headless: true }The banner was closed.
consent:given{ decision, categories, source }A decision other than “reject all” was stored locally.
consent:rejected{ decision, categories, source }”Reject all” was stored locally.
consent:changed{ oldCategories, newCategories }A decision replaced an earlier one.
consent:withdrawn{ categories, cleanedCookies }CMP.withdraw() was called.
consent:synced{ consent_token, idempotency_key }The server accepted a record (immediately or from the queue).
consent:error{ code, status, message, idempotency_key, pending_sync, final?, reprompt? }A record could not be delivered. final: true means it was dropped.
document:approved{ slug, version, consentId }A document approval was recorded.
document:rejected{ slug, version }The visitor declined a document.
document:error{ slug, version, code, status, message }A document could not be loaded or the approval was not recorded.
scripts:activated{ category, count }Tagged scripts of a category were activated.
config:updated{ content_hash }A newer configuration replaced the cached one.
language:changed{ from, to }The language changed.
error{ scope, code, status, message, … }Any of the errors above; scope is config, consent, module or document.

consent:given / consent:rejected fire when the decision is stored in the browser. Use consent:synced (or the promise result) when you need to know that the server has it.


Codes produced by the SDK itself:

CodeMeaning
NOT_READYA decision method was called before the configuration was loaded. Call it inside CMP.ready().
COOKIE_POLICY_MISSINGNo published cookie policy document is attached to the site. The banner is not shown, non-essential scripts stay blocked, and the decision is neither stored nor queued.
NETWORK_ERROR, TIMEOUTNo response from the API (timeout: 5 s per attempt).
HTTP_<status>The server answered with an error that carries no machine code.
DOCUMENT_VERSION_CHANGEDThe referenced document version is no longer the active one; the record was dropped.
PENDING_EXPIREDA queued decision could not be delivered within 7 days and was dropped.
MODULE_LOAD_FAILEDA module file (documents or preferences) could not be loaded. Also reported through the error event with scope: "module".
APPROVAL_TOKEN_REQUIREDThe document requires a one-time approval token and none was passed (also returned by the server).
CONFIG_UNAVAILABLEThe configuration could not be loaded (reported through the error event).
UNEXPECTED_ERRORAnything else.

Codes returned by the server are passed through unchanged. The most relevant ones are listed on the REST API page — for example CONFIG_OUTDATED, SITE_NOT_VERIFIED, ORIGIN_NOT_ALLOWED, RATE_LIMIT_EXCEEDED, CONSENT_LIMIT_EXCEEDED, APPROVAL_TOKEN_INVALID.

Temporary server responses. 503 (API_KEY_LOOKUP_UNAVAILABLE, ADMISSION_UNAVAILABLE, QUEUE_OVERLOADED) and 429 RATE_LIMIT_EXCEEDED are retried automatically with the same idempotency_key; the SDK waits for the time given in the Retry-After header. If that time is longer than 5 seconds, the SDK does not wait inside the page: the decision stays in the local queue (pending_sync: true) and is sent later. After 429 TOO_MANY_INVALID_API_KEYS the SDK sends no further requests during that page view and queued decisions are kept; check the data-site value. None of these responses affects your page: the error event fires and CMP.ready() callbacks still run.

Section titled “Temporary server responses. 503 (API_KEY_LOOKUP_UNAVAILABLE, ADMISSION_UNAVAILABLE, QUEUE_OVERLOADED) and 429 RATE_LIMIT_EXCEEDED are retried automatically with the same idempotency_key; the SDK waits for the time given in the Retry-After header. If that time is longer than 5 seconds, the SDK does not wait inside the page: the decision stays in the local queue (pending_sync: true) and is sent later. After 429 TOO_MANY_INVALID_API_KEYS the SDK sends no further requests during that page view and queued decisions are kept; check the data-site value. None of these responses affects your page: the error event fires and CMP.ready() callbacks still run.”

When the banner is shown again. The stored consent remembers the content hash of the banner configuration it was given for. The banner is shown again when:

  • the consent has expired (the validity period is set in the dashboard),
  • the category / cookie structure of the banner changed, or
  • a new version of an attached document was published — unless the banner’s reconsent_on_doc_change setting is turned off, in which case only structure changes trigger re-consent.

While re-consent is pending in an opt-in region, the visitor’s previous choice stays in effect until they decide again.

Pending decisions and a changed document version. If a decision could not be delivered (offline, page closed, server error) and, when it is retried, the document version it references is no longer the active one, the SDK:

  1. asks the server for the current configuration, bypassing the cache;
  2. if the version is unchanged (the 409 was caused by a cold server cache), sends the same record once more;
  3. if the version changed, drops the record, removes the locally stored consent and shows the banner again (consent:error with code: "DOCUMENT_VERSION_CHANGED", final: true, reprompt: true).

A pending record is never re-sent with a different document version: the visitor did not see that version.


Regions, regulation profiles and behaviour flags

Section titled “Regions, regulation profiles and behaviour flags”

The visitor’s region is resolved in this order: CMP.setRegion() → data-region → the server’s GeoIP result in the configuration response (detected_region) → an estimate from the browser time zone → the banner’s fixed regulation mode → the strictest rules (GDPR). The SDK never calls a third-party geolocation service.

RegionFrameworkModelBehaviour
EU / EEA, Switzerlandgdpropt-inBanner; tagged scripts blocked until allowed.
Türkiyekvkkopt-inSame as GDPR.
United Kingdomuk_gdpropt-inSame as GDPR (see uk_analytics_exemption).
Brazillgpdopt-inSame as GDPR.
South Africa, Canada, China, South Koreapopia, pipeda, pipl, pipaopt-inGDPR banner rules are applied.
USAccpa — only if enabled, otherwise gdpropt-out / opt-inSee ccpa_opt_out_enabled.
Unknowngdpropt-inStrictest rules.
none (explicit)none—No banner; all scripts run.

In every opt-in profile the banner always has a “Reject” button with the same visual weight as “Accept”, and no optional category is pre-selected — regardless of what the banner design says.

Three behaviour flags (banner → behaviour settings in the dashboard, banner_config.behavior in a local configuration) change this:

FlagDefaultEffect
uk_analytics_exemptionoffUK visitors only: scripts tagged analytics may run before a decision. Every other category still needs consent, and once the visitor has decided, their decision applies.
ccpa_opt_out_enabledoffVisitors detected in the USA get the opt-out model: scripts run by default, a “Do Not Sell or Share My Personal Information” link is shown, and a Global Privacy Control signal is applied once as an opt-out (consent_type: do_not_sell_opt_out). When off, US visitors get the opt-in banner.
reload_on_revokeonReload the page when a visitor revokes a category whose scripts already ran on this page. Turn it off only if your tags react to consent changes themselves.

When Consent Mode is on, the SDK creates window.dataLayer and a gtag function if they do not exist and pushes:

  1. gtag('consent', 'default', { …all denied…, wait_for_update: 500 }) during start-up,
  2. gtag('consent', 'update', { … }) after every decision and on each page load with a stored consent.
SignalGranted when this category is allowed
ad_storage, ad_user_data, ad_personalizationmarketing
analytics_storageanalytics
functionality_storage, personalization_storagepreferences
security_storagealways

The SDK also pushes the data layer events hashentry_consent_default and hashentry_consent_update.

Setup:

  1. Turn on Google Consent Mode for the banner in the dashboard, or add data-consent-mode="true" to the script tag.
  2. Place the HashEntry script tag above your Google tag / GTM snippet.
  3. If the default command must precede Google tags on a visitor’s very first page view, use data-consent-mode="true" and load cmp.js without async. The dashboard setting arrives with the configuration, which on a first visit is loaded asynchronously; on later visits it is read from the cached configuration during start-up.

Consent Mode only sends signals. Tags that are not controlled by Consent Mode still need to be tagged for blocking as described above.


With data-headless="true" the SDK renders nothing. It still loads the configuration, blocks tagged scripts, updates Consent Mode and records decisions.

CMP.on('banner:shown', ({ headless, reason, config }) => {
if (headless) openMyBanner(config); // reason: no_consent | expired | reconsent | withdrawn | api | opt_out
});
myAcceptButton.onclick = async () => {
const result = await CMP.accept({ analytics: true, marketing: false });
if (!result.success && !result.pending_sync) showError(result.error);
};

ModeConfiguration source
remoteServer only. data-site is required.
localThe page only (CMP.setConfig, data-config or <script type="application/json" id="cmp-config">).
local-fallback (default)Server; if it cannot be reached, the page configuration.

The mode only selects where the configuration comes from. Decisions are sent to the server in every mode as long as data-site is set. In local mode without data-site, decisions are kept in the browser only (local_only: true).

A local configuration uses the same shape as the server response:

<script type="application/json" id="cmp-config">
{
"banner_config": {
"layout": "bottom_bar",
"theme": { "primary_color": "#2563EB", "font_family": "system", "theme_mode": "auto" },
"behavior": { "show_widget": true, "show_reject_button": true, "show_settings_button": true },
"categories": [
{ "key": "necessary", "required": true, "default": true },
{ "key": "analytics", "required": false, "default": false,
"cookies": [{ "name": "_ga", "provider": "Google", "type": "cookie", "duration": "2 years" }] }
],
"languages": {
"default": "en",
"translations": {
"en": {
"title": "Cookie preferences",
"description": "We use cookies to measure how the site is used.",
"accept_button_text": "Accept all",
"reject_button_text": "Reject all",
"settings_button_text": "Manage preferences",
"save_selected_text": "Save selection",
"modal_title": "Cookie settings",
"always_active_text": "Always active",
"categories": {
"necessary": { "label": "Necessary", "description": "Required for the site to work." },
"analytics": { "label": "Analytics", "description": "Visitor statistics." }
}
}
}
}
}
}
</script>
<script src="https://tool.hashentry.com/cmp.js" data-site="YOUR_SITE_API_KEY" data-mode="local" async></script>

With data-site set, the SDK asks the server once at start-up for the cookie policy document, because a consent must reference its version. If none is attached, the banner is not shown (COOKIE_POLICY_MISSING). To show the banner even when the server cannot be reached, include documents (the cookie policy and its version) in the page configuration.


All banner texts (title, description, buttons, category names) come from the banner configuration — they are not part of the script.

The SDK ships a small built-in dictionary, in English and Turkish only, for texts that belong to its own UI: the “Do Not Sell” dialog, the document approval buttons and error line, the cookie table headings and the close button. In any other language these texts fall back to English unless you provide them:

CMP.setLocale('de', {
document: { approve: 'Ich stimme zu', reject: 'Ablehnen', saving: 'Wird gespeichert…', error: 'Ihre Zustimmung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.' },
ccpa: { link: '…', description: '…', cancel: 'Abbrechen', optOut: 'Widersprechen' },
cookies: { name: 'Name', provider: 'Anbieter', type: 'Typ', duration: 'Dauer', purpose: 'Zweck', category: 'Kategorie', count: '{n} Cookies', empty: 'Keine Cookies registriert.' }
});

A configuration can carry the same keys per language under languages.translations.<lang>.sdk_texts.


FileContentLoaded
cmp.jsCore and cookie consentBy your script tag.
cmp-documents.<hash>.jsDocument approval (showDocumentApproval, approveDocument)By cmp.js, the first time one of these methods is called.
cmp-preferences.<hash>.jsPreference centre, cookie details table, “Do Not Sell” confirmation dialogBy cmp.js, the first time the visitor opens one of them.

You never add a second script tag. A site that only uses the cookie banner never downloads the document module.

  • The module file is requested from the same location as cmp.js (derived from the script’s src).
  • To avoid the short delay on the first call, load it up front: data-modules="documents".
  • Calls made before the module has arrived simply wait for it. If it cannot be loaded, the method resolves with success: false and error.code = "MODULE_LOAD_FAILED"; the next call tries again.
  • If the preferences module cannot be loaded, the banner stays on screen with its accept and reject buttons, the error event fires with scope: "module" and code: "MODULE_LOAD_FAILED", and the next click tries again. To open the preference centre without any delay, load it up front: data-modules="preferences".
  • The file name contains a content hash that is embedded in cmp.js at build time, so cmp.js always asks for the module built with it.

Content Security Policy. The module is loaded with a <script> element from the same origin as cmp.js; the SDK uses neither eval nor inline scripts. For scripts, the SDK origin is all you need:

script-src https://tool.hashentry.com;
connect-src https://system.hashentry.com;
  • The SDK styles its UI with <style> elements inside its own shadow root. If your policy restricts style-src, either allow 'unsafe-inline' for styles or use a nonce: put the nonce on the cmp.js tag (<script nonce="…" …>) and the SDK copies it to the style elements and to the module script tag it creates.
  • With script-src 'strict-dynamic' and a nonce on the cmp.js tag, the module script is allowed without listing the origin.
  • The document approval dialog renders the document in a sandboxed iframe without a src; scripts inside the document do not run.

Subresource Integrity. https://tool.hashentry.com/cmp.js is updated in place when a new SDK version is released, so an integrity attribute on that URL would break on the next release. If you need SRI, host a reviewed copy of cmp.js together with the cmp-documents.*.js / cmp-preferences.*.js files of the same build on your own origin and set integrity on your own tag.

Self-hosting. Copy cmp.js, cmp-documents.<hash>.js, cmp-preferences.<hash>.js, cmp-preferences.js and cmp-documents.js from the same build into one folder. If the module files live elsewhere, set data-module-base.


To send API requests through your own domain, set data-api-url (or CMP.setApiUrl) to a path or URL on your side and forward it to the HashEntry API without changing the path below the prefix:

<script src="https://tool.hashentry.com/cmp.js"
data-site="YOUR_SITE_API_KEY"
data-api-url="/hashentry-api" async></script>
# /hashentry-api/tool/v1/consents → https://system.hashentry.com/tool/v1/consents
location /hashentry-api/ {
proxy_pass https://system.hashentry.com/;
proxy_set_header Host system.hashentry.com;
proxy_ssl_server_name on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

The request signature covers the path /tool/v1/..., so the upstream request must arrive with exactly that path. The recorded IP address is the one HashEntry sees; whether the visitor’s address is taken from X-Forwarded-For depends on the proxy being trusted on the HashEntry side — ask support before relying on it.


NameWherePurpose
_he_consentfirst-party cookieThe visitor’s decision (categories, timestamps, server token).
he_consent_tokenlocalStorageCopy of the same decision.
he_config_cachelocalStorageCached configuration (up to 7 days, revalidated in the background on every load).
he_pending_consentslocalStorageDecisions not yet accepted by the server (removed once delivered; dropped after 7 days).
he_gpc_appliedlocalStorageMarks that a GPC opt-out was applied once.
he_sdk_reloadsessionStorageGuards the SDK self-update against reload loops.

The SDK does not load fonts or any other resource from third parties.