Skip to content

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.

  • 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.
import { grantConsent, revokeConsent, getConsentState } from '@syntarie/tracking';
getConsentState(); // 'unknown' | 'granted' | 'revoked'
StateBehaviour
'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.

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.

With server-side consent enforcement on:

  • Requests without X-Consent return 403 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.

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.

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.

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.

Every consent-related decision (granted/revoked, accepted/rejected) is recorded so you can assert on rejection rates as part of your privacy compliance posture.