Consent & compliance
Tocadule tracks consent on two independent lanes, sent on every event:
| Lane | Field on the event | What it gates |
|---|---|---|
| Analytics | consent_state | Whether we store the visitor's anonymous_id and personal data (PII) at all |
| Advertising | ad_consent | Whether — and how much of — an event is forwarded to ad destinations (Meta, Google Ads, TikTok) |
These are separate on purpose: a visitor can allow analytics but deny advertising, and the two decisions never leak into each other. A newsletter/SMS opt-in is a third, unrelated thing — a merchant preference, not a CMP lane (see Email & SMS subscription).
Each lane carries one of three values:
granteddeniedunknown— no CMP signal detected (the default before a banner resolves)
Anything else the snippet might send is normalized to null server-side, which means "defer to the workspace's configured posture."
The analytics lane — consent_state
consent_state decides whether we keep an identifiable record of the visitor.
We store unless the lane is explicitly denied. granted, unknown, and null all store. Only an explicit denied turns storage off. Concretely, when the snippet is in analytics-denied ("cookieless") mode:
- No
anonymous_idis written tolocalStorageor cookies, and none is sent on events — the backend counts the visit through a daily-rotatingvisitor_session_hash(IP + user-agent) instead, so analytics still work without a persistent identifier. - The analytics browser ID we bake (
_ga) is removed. - Popup throttle/impression state moves from
localStoragetosessionStorage(session-scoped, treated as strictly-necessary). - Latitude / longitude are only attached when
consent_state === 'granted'. Country / region / city / postal are always resolved (aggregate-level, needed for the dashboards); precise lat-lon is gated. - A visitor who denies analytics but later shares an email on an event will not be auto-linked to a contact — the event-identity upsert is skipped when the analytics lane is
denied.
identify() is an explicit PII share (login, sign-up, opt-in), so calling it grants the analytics lane even if the banner was previously denied. It does not grant the advertising lane — sharing your email for a receipt is not opting into ad targeting.
The advertising lane — ad_consent
ad_consent never affects your dashboards. It only controls what leaves Tocadule for ad platforms, and it does so through three send treatments, resolved from the lane value plus the workspace posture:
ad_consent | Treatment | What is sent |
|---|---|---|
granted | full | Full server-side event incl. hashed PII + click IDs — highest match quality |
unknown (workspace permissive) | full or ldu | Destination's default button decides |
unknown (workspace cookieless) | modeled | Treated as non-consented |
denied | modeled | Conversion signal only |
- full — everything: hashed email/phone/name + click IDs.
- ldu — Limited Data Use: strip PII, keep click IDs (attribution still works, nothing personal).
- modeled — strip PII and click IDs; a minimal, non-persistent conversion signal only. (Google Ads has no identifier-less upload path, so
modeledthere means skip — nothing is sent.)
On the browser side, denying the ad lane also deletes the ad browser-IDs we baked (_fbp, _ttp, _fbc). IDs the merchant's own pixel set are left alone — they belong to the merchant's own consent flow.
GA4 is gated by the analytics lane, not this one. GA4 is measurement, not advertising, so its server-side forwarding is keyed off consent_state — only Meta, Google Ads, and TikTok are gated by ad_consent. GA4 runs the same treatment ladder (full / ldu / modeled), where modeled sends Google Consent Mode "denied" flags rather than being skipped.
GPC is a hard ad-denial. If the browser sends a Global Privacy Control signal (navigator.globalPrivacyControl === true), the advertising lane is forced to denied regardless of any CMP state.
How the snippet detects consent
You usually don't wire anything up. The snippet reads consent from whatever CMP is already on the page, checked in priority order and re-checked after 500 ms, 2 s, and on the CMP's own change events:
- An explicit override —
window.tocadule = { _consent: 'granted' }/_ad_consent, or a<meta name="tocadule:consent">/tocadule:ad_consenttag - IAB TCF v2 (OneTrust, Cookiebot, Quantcast, Sourcepoint, Didomi, …)
- Google Consent Mode v2 — reads the resolved
analytics_storage/ad_storagefromgoogle_tag_data.ics, so it's CMP-agnostic (efilli, cookieseal, Cookiebot, OneTrust, Usercentrics…) - GPC (ad lane only — hard denial)
- Vendor-specific fallbacks: Iubenda, OneTrust
OptanonConsentgroups (C0002analytics,C0004advertising), Cookiebot categories
If nothing resolves, both lanes stay unknown and the workspace's configured posture (permissive vs cookieless) decides behavior.
Driving consent yourself (no CMP)
If you don't run a CMP, drive the lanes directly from your own banner:
// Analytics lane
tocadule.consent.set('granted'); // or 'denied' | 'unknown'
tocadule.consent.get();
// Advertising lane — independent of analytics
tocadule.consent.setAd('granted'); // or 'denied' | 'unknown'
tocadule.consent.getAd();Calling set/setAd re-applies tracking mode immediately (bakes or clears IDs, switches storage), so it's safe to call the moment the visitor clicks your banner.
What goes on the wire
Every track() and identify() request carries both lanes — you never set them per call:
// track() body
{ workspace_id, anonymous_id, event, properties, consent_state, ad_consent }
// identify() body
{ workspace_id, anonymous_id, email, traits, user_language, consent_state, ad_consent }tocadule.cart() is PII-free by design — it carries the anonymous_id and the cart contents only, and resolves to a contact through the identity map. Never put an email on the cart; the cart payload has no consent fields of its own because it stores no personal data.
Email & SMS subscription is not a consent lane
Marketing subscription (subscribed_email, subscribed_sms, subscribed_phone) is a merchant preference, completely separate from the two CMP lanes above. It governs whether you may send campaigns to a contact — not whether you may track them.
- An email on an event is not a subscription. When an event carries an email and we auto-create the contact, it is created
subscribed = false. Opt-in only comes from an explicit source. identify()can set subscription intent via traits — per-channel (subscribed_email/subscribed_sms/subscribed_phone) or a singleconsent_etkcheckbox that maps to all three. The mastersubscribedflag isOR(email, sms, phone).- A preference center fires a
preference_updatedevent with the per-channel booleans to update state later.
// Sign-up with explicit opt-in
tocadule.identify('shopper@example.com', {
first_name: 'Ada',
subscribed_email: true,
subscribed_sms: false
});Identity, dedup & geo (compliance notes)
Linking a visitor to a contact needs at least one signal, and there are two valid paths:
identify()once — links the contact and backfills the visitor's prior anonymous events.- An email (or phone / name /
external_id) on an ordinary event — links from that event forward only (no backfill).
Either is fine; you don't need to call identify() before every event. See Sending events for the field contract.
Deduplication is automatic. Browser events dedupe on anonymous_id; external/server events dedupe on source_event_id. Merchants never set dedup IDs.
Location is handled two ways, deliberately:
- Event location is resolved from the real client IP via MaxMind (offline GeoLite2) — the single source of truth for country / region / city / postal / timezone (and lat-lon when analytics consent is
granted). Location segments readproperties.countryoff events. - Contact-profile location is declared-only. It is set solely from
identify()traits (country,city) — never inferred from an IP. An IP says where a request came from (proxy / VPN / travel), not where a person lives, so we never freeze an inferred country onto a contact.
Custom events
Any event name is accepted — tocadule.track('wishlist_added', {...}) stores exactly as sent. Unknown names are treated like every other event: written to the events table, visible in the raw event stream on the Events page, and usable in segments and automations by their literal name.
What they don't get is the built-in canonical treatment: only recognized names (and their aliases) roll up into the Data Collection cards, the commerce funnel, and the ad-platform gap report, and only recognized commerce events map to a standard destination event (Meta AddToCart, GA4 view_item, …). Prefer the standard event names when a matching one exists; reach for a custom name only when nothing fits.