Consent gating
Consent is enforced server-side at the ingest endpoint. The browser SDK
queues events while consent is 'unknown' and refuses to send while it is
'revoked', but a deliberately crafted client request that bypasses the
SDK is still rejected on the server when consent enforcement is on.
This page covers both layers.
Defaults
Section titled “Defaults”- The browser SDK defaults to
defaultConsent: 'unknown'. Events queue in memory; nothing leaves the page. - Server-side consent enforcement is on by default for EU traffic. When it is on, an event that arrives without proof of consent is rejected before it is ever stored.
Client-side states
Section titled “Client-side states”import { grantConsent, revokeConsent, getConsentState } from '@leadmaps/tracking';
getConsentState(); // 'unknown' | 'granted' | 'revoked'| State | Behaviour |
|---|---|
'unknown' (default) | Events queue in memory. Nothing is sent. |
'granted' | Queued events drain. Subsequent sends go straight to the ingest endpoint. |
'revoked' | The in-memory queue is purged. Subsequent sends are no-ops for the rest of the page. |
The 'unknown' to 'granted' transition happens when your consent UI calls
grantConsent(). The 'granted' → 'revoked' transition happens when
the user opts out (calls revokeConsent() from your settings UI).
The visitor can grant again after revoking. Use a fresh consent token for the new decision. The managed leadmaps banner rotates that token automatically.
Managed consent banner
Section titled “Managed consent banner”Sites in Consent privacy mode can use the managed banner from the dashboard’s site Install screen. The saved design is embedded in the copied snippet, so the browser does not fetch banner settings from leadmaps before consent.
The editor supports safe copy, languages, colors, placement, opacity, blur, radius, width, font, shadow, button order, and a privacy-settings trigger. It does not accept raw HTML, CSS, scripts, selectors, remote fonts, or image URLs. Both Accept and Decline always remain available.
See Custom consent banner for setup, migration, external-platform integration, and withdrawal controls.
The X-Consent header
Section titled “The X-Consent header”grantConsent(token?) accepts an optional opaque token. Consent-mode hosted
installs require a non-empty token. When provided,
the SDK attaches it as X-Consent: <token> on subsequent consent-mode
tracking, replay, configuration, and operational-diagnostic requests:
grantConsent('eyJjaWQiOiIuLi4iLCJ0aW1lc3RhbXAiOi4uLn0=');The token shape is yours to define. It is opaque to the SDK and to leadmaps. Common choices:
- A signed JSON object with
(consentId, timestamp, granted_categories). - A short opaque id you persist alongside the user’s audit trail.
leadmaps persists the value on accepted tracking events and replay chunks so an audit trail is reconstructable. Operational Experience diagnostics validate the proof but deliberately do not retain it.
Server-side enforcement
Section titled “Server-side enforcement”With server-side consent enforcement on:
- Requests without
X-Consentreturn403 consent_required. - For tracking events and replay chunks, the header value is persisted alongside the accepted record (subject to the normal length cap).
- Operational Experience diagnostics validate the header but do not store it.
The check runs in front of every storage write, including ingest from the Node SDK and from webhook adapters. There is no path for an event to reach storage without the header when enforcement is on.
DNT (Do Not Track)
Section titled “DNT (Do Not Track)”The browser SDK honours navigator.doNotTrack === '1' by default. A
DNT-blocked init never installs listeners and every subsequent send is a
no-op. The DNT check happens before consent, so a DNT-on user does not
even reach the consent state machine.
To opt out of DNT respect (e.g. you are showing a UI that explicitly asks the user even when DNT is on):
init({ siteId, host, respectDnt: false });This is a privacy regression. Gate it on a deliberate policy decision.
What about pageviews queued before consent?
Section titled “What about pageviews queued before consent?”The first pageview lands at init() time. If defaultConsent is
'unknown', that pageview is queued in memory along with everything else.
On grantConsent() it drains in order. No event leaves the page during
the 'unknown' phase.
If the user never grants consent, the queued events are dropped on
pagehide and never reach storage.
Experience, heatmaps, and replay
Section titled “Experience, heatmaps, and replay”Experience uses a stricter pre-consent boundary than the core pageview queue. Until consent is granted, the SDK makes no Experience configuration request, does not import the Experience capture code, installs no interaction listeners, and creates no replay buffer. There is therefore no retained pre-consent Experience activity to send later.
After consent, the SDK fetches the current site policy and refreshes it while the page remains active. A missing, failed, or invalid initial policy response leaves Experience disabled. A failed later refresh stops active capture until a subsequent policy request succeeds. Revoking consent removes active listeners and clears active Experience and replay buffers and capture state.
Anonymous privacy mode never enables Experience, replay, individual journeys, or cross-day returning-visitor analysis.
What about already-stored anon IDs?
Section titled “What about already-stored anon IDs?”grantConsent does not retroactively un-anonymize stored events. leadmaps
hashes the IP per event regardless of consent state; there is no
“now you’ve consented, attach your real IP” path. This matches the GDPR
notion that data minimization at collection time is more robust than
retroactive scrubbing.
Audit trail
Section titled “Audit trail”Every consent-related decision (granted/revoked, accepted/rejected) is recorded so you can assert on rejection rates as part of your privacy compliance posture.