Consenti

How Consent Flow Works

Understanding the consent lifecycle makes it easier to build features around it — gating analytics, reacting to preference changes, or triggering re-consent. This guide walks the full journey from a visitor's first page load to a stored consent record.

The lifecycle at a glance

1. Page loads → new ConsentiSetup() called
2. Widget checks for an existing consent cookie
3. No cookie → profile resolved → banner injected
4. Visitor clicks "Accept All" (or customises)
5. Cookie written to browser
6. consenti:consentSubmitted event fires on window
7. Next page load → cookie found → banner stays hidden

Stage 1 — Widget initialisation

The moment new ConsentiSetup(config) runs, the widget checksdocument.cookie for a cookie matching the pattern consenti_*.

  • Cookie found and valid — widget initialises silently. No banner. The consenti:bannerInitialized event fires with { visible: false }.
  • No cookie (first visit) or cookie expired — widget proceeds to profile resolution.
typescript
const widget = new ConsentiSetup({ compliance: { type: 'opt-in' } })

// Called once widget is fully initialised — regardless of banner visibility
widget.onReady(() => {
  console.log('Has prior consent?', widget.hasConsent())
})

Stage 2 — Profile resolution

Without an existing consent record, Consenti downloads a compliance profile — the data that describes the banner layout, button labels, cookie categories, and GPC settings.

Profiles can come from three places (in priority order):

  1. API-backed (api.enabled: true) — the widget calls your backend's /resolve-profile endpoint, which returns a URL to the right locale JSON file.
  2. Fixed group (compliance.type: 'opt-in') — the widget directly loads the matching pre-built profile chunk. No network call beyond the chunk itself.
  3. Client-side auto (compliance.type: 'auto', no API) — the widget reads Intl.DateTimeFormat().resolvedOptions().timeZone and navigator.language, maps them to a compliance group, then loads that pre-built chunk.
ℹ️Profile JSON is cached in sessionStoragefor the tab's lifetime. Navigating between pages never re-fetches the profile.

Stage 3 — Banner shown

Once the profile is loaded, the widget injects the banner into the DOM (appended to document.body by default, or your custom rootEl). The consenti:bannerVisibility event fires with { visible: true, type: "main" }.

If GPC is active and the profile's gpcMode is 'honor' or 'strict', a GPC banner variant appears instead (or no banner at all for 'strict').

Stage 4 — Visitor interacts

The visitor has three paths:

  • Accept All — grants all cookies in the profile.
  • Reject Optional — grants only mandatory cookies; denies everything else.
  • Customise — opens the preference modal where each category can be toggled individually.

When a button is clicked, consenti:consentBeingSubmitted fires immediately (before the cookie is written). Use this to show a loading indicator if needed.

Stage 5 — Cookie written

After the visitor's choice is captured, Consenti writes a single cookie:

text
consenti_{userId}_{profileId}=t:{timestamp}::{cookieId}:{status}|{cookieId}:{status}|…

Example:

text
consenti_a1b2c3_default=t:1782133054::analytics:granted|marketing:denied|necessary:granted

The cookie lifetime equals the profile's expiryDays (default 365, profile-wide — not set per cookie). When it expires, the next visit triggers re-consent automatically.

💡Enable HMAC signing for tamper detection by setting core.cookieSigningKey to a 32+ character secret. A ::sig:{hmac} suffix is appended to the cookie value and verified on every read.

Stage 6 — Event fires

Once the cookie is written, consenti:consentSubmitted fires on window. This is your hook to enable or disable third-party scripts based on the visitor's choices.

typescript
// Using the typed widget method (recommended)
widget.on('consentSubmitted', (data) => {
  const { consentJson } = data
  // consentJson = { analytics: 'granted', marketing: 'denied', necessary: 'granted' }

  if (consentJson.analytics === 'granted') {
    initGoogleAnalytics()
  }

  if (consentJson.marketing === 'denied') {
    disableAdPixels()
  }
})

// Or a raw DOM listener
window.addEventListener('consenti:consentSubmitted', (e) => {
  const { consentJson } = e.detail
})

Stage 7 — Subsequent visits

On every page load after the initial consent, Consenti reads the cookie, validates it (checks the consent's profileIdstill matches the compliance group's currently active profile id, the expiryDays window, and the HMAC signature if signing is enabled), and if valid suppresses the banner entirely. The widget is still initialised and available for programmatic use — widget.getConsent(), widget.showModal(), etc.

⚠️If you edit your profile (add new categories, change cookie IDs), a new profile id becomes active and the stored consent no longer matches it — verification returns profile_changed and the banner re-shows automatically. Call widget.reConsent() to force this manually.

Checking consent in your own code

typescript
// Gate a feature on a single cookie
if (widget.isCookieGranted('analytics')) {
  initAnalytics()
}

// Read the full consent record
const consent = widget.getConsent()
// { analytics: 'granted', marketing: 'denied', necessary: 'granted' }

// Check when the visitor last consented
const date = widget.getConsentDate()  // Date | false

Frequently asked questions