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 '@syntarie/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' → '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).
There is no 'revoked' → 'granted' re-grant inside a single page
lifetime. After revoke, the user must reload and grant fresh.
The X-Consent header
Section titled “The X-Consent header”grantConsent(token?) accepts an optional opaque token. When provided,
the SDK attaches it as X-Consent: <token> on every subsequent request:
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 every accepted event so an audit trail is reconstructable.
Server-side enforcement
Section titled “Server-side enforcement”With server-side consent enforcement on:
- Requests without
X-Consentreturn403 consent_required. - The header value is persisted alongside the event (subject to the normal length cap).
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.
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.