Methodology

Consent Mode v2 + Meta Limited Data Use — propagating consent end-to-end through the edge fanout

Methodology for honoring user consent state from the browser through the CRM through the Cloudflare Worker to each of the five platform APIs. Google Consent Mode v2 fields, Meta Limited Data Use, Microsoft consent_mode, TikTok processing_options, plus the CRM-side consent flag pattern.

The fastest way to turn a compliance asset into a liability is to honor consent in the browser and forget about it everywhere else. Server-side attribution moves your conversions out of the browser — so your consent signal has to make the same trip.

Consent propagation is the fourth of the five properties of a working server-side conversion attribution stack, and it is the one most likely to be bolted on last and break first. The fix is a single canonical consent-state object that travels with every event — browser to CRM to Worker to each platform's documented field — translated per platform at the Worker boundary, not at the cookie banner. Here is the object, the per-platform translations, the CRM-side flag that catches withdrawals, and the six ways it breaks.

Why it is non-negotiable

Compliance. GDPR, CCPA, and the wave of US state privacy laws treat sending hashed PII to a conversion API as processing personal data. Firing the event for a user who withheld consent is a breach with enforceable penalties.

Trust. A user who clicks "reject" and then watches their data flow to ad platforms anyway treats you as untrustworthy — the unmeasurable variable the consent layer protects.

Bidder signal. The platforms now suppress non-consented signal themselves. Google's Consent Mode v2 reads ad_user_data: denied and excludes those events from optimization. Firing without consent does not just risk compliance — it degrades the optimization surface for the consented events arriving alongside.

One object, defined once on the canonical event schema, traveling with every event:

type ConsentState = {
  ad_storage: 'granted' | 'denied'           // marketing/ads cookies
  analytics_storage: 'granted' | 'denied'    // analytics cookies
  ad_user_data: 'granted' | 'denied'         // sharing user data with ad partners
  ad_personalization: 'granted' | 'denied'   // personalized advertising
  region: string                             // ISO 3166-2 (us-ca, eu-de, …)
  consent_collected_at: number               // Unix seconds
  consent_method: 'banner' | 'preference_center' | 'implicit' | 'pre_consent'
  consent_record_id: string                  // CMP-side audit primitive
}

Four of those fields are Google's Consent Mode v2 vocabulary; mapping every banner's output to them at the canonical boundary lets every downstream translation use the same four-signal frame. region carries the ISO 3166-2 subdivision so the Worker can apply California-specific gates like Meta's Limited Data Use. consent_collected_at stamps the decision, and pre-consent traffic (the window between page load and decision) is flagged pre_consent and never fires events. consent_record_id is the audit chain — event_id → consent_record_id → CMP record → user action — the difference between proving a banner exists and proving a specific user consented.

Mandatory for EEA/UK/Switzerland traffic since March 2024 and widely adopted globally, Consent Mode v2 carries two of its four signals directly in the server-side request (the other two are browser cookie-storage signals that do not travel server-side):

function canonicalToGoogleConsent(c: ConsentState) {
  return {
    ad_user_data: c.ad_user_data === 'granted' ? 'GRANTED' : 'DENIED',
    ad_personalization: c.ad_personalization === 'granted' ? 'GRANTED' : 'DENIED',
  }
}

A DENIED value does not reject the upload — Google still receives the conversion for aggregate reporting but excludes it from personalized bidding and audience expansion. The count survives; the per-user attribution drops.

Meta Limited Data Use — gated on California + opt-out

Meta's Limited Data Use is the CCPA-specific path. The Conversions API request carries a data_processing_options array plus country and state codes, and the gate has to be precise — firing LDU on every event throttles every conversion's signal, including for users outside California:

function canonicalToMetaLDU(c: ConsentState) {
  const isCalifornia = c.region?.startsWith('us-ca')
  const optedOut = c.ad_user_data === 'denied' || c.ad_personalization === 'denied'
  return isCalifornia && optedOut
    ? { data_processing_options: ['LDU'], data_processing_options_country: 1, data_processing_options_state: 1000 }
    : { data_processing_options: [] }
}

(country: 1 is the US; state: 1000 is California.) LDU is effective per-event, not per-pixel — the same pixel fires LDU and non-LDU events and Meta handles the distinction.

Microsoft, TikTok, GA4 — the remaining three

Each has a consent-equivalent field, mapped from the same canonical object. Microsoft Ads takes a consent_mode object carrying the four Google-equivalent signals directly. GA4's Measurement Protocol takes a consent object with ad_user_data and ad_personalization — honoring denied by excluding the event from Google Ads conversion linking and Google Signals. TikTok's Events API is the odd one out, taking a processing_options array of limitation codes:

function canonicalToTikTokConsent(c: ConsentState) {
  const limitations = []
  if (c.ad_user_data === 'denied') limitations.push('LDU')       // Limited Data Use
  if (c.ad_personalization === 'denied') limitations.push('LMU') // Limited Match Use
  return { processing_options: limitations }
}

Want consent propagated correctly the first time? Talk to the team that runs it under audit. →

The subtle failure: consent diverging between the browser and the CRM. A user grants at the banner, the CRM record is created, then the user visits a preference center and withdraws — and the Worker keeps firing, because it read consent from the browser at event time and the browser is no longer the source of truth. The fix is a CRM-side consent_state field on the contact, updated by the CMP, and the Worker treating a CRM-side withdrawal as authoritative. In HubSpot this is the native marketing_consent_status plus a custom JSON property carrying the four-signal vocabulary; in Salesforce, HasOptedOutOfEmail plus a custom Consent_State_Object__c. The webhook fires on contact updates; the Worker reads both and short-circuits the fanout if either signals denial.

The six failure modes

  • Defaulting to grant on missing fields. An undefined signal should resolve to denied, not granted. Default to denied.
  • Ignoring CRM-side withdrawal. The freshest consent for a returning user lives on the CRM record, not the browser.
  • Firing during the pre-consent window. The 100–500ms before a decision is made — short-circuit on the pre_consent flag.
  • Applying LDU indiscriminately. Gate it on California residency plus opt-out, not as a blanket safety position.
  • Stale consent_collected_at on re-consent. A user who denies, returns, and grants must get a fresh timestamp, or the audit fails.
  • Logging consent alongside PII. Pairing the consent record ID with hashed identifiers in logs creates a derived identifier. Treat the consent layer with the same logging discipline as PII.

Closing

Consent propagation is the most subtle of the five properties because the patterns themselves are simple — the platform fields are stable, the translation functions are pure. What makes it hard is the coordination across the banner, the CRM, the Worker, and the platform APIs, each with its own representation of consent and its own update cadence. Get the canonical object right and every downstream translation falls out as a pure function. Skip it and every integration grows its own consent logic and drifts. The boring stack that prints keeps consent in one place and translates at the edge.

Ready to ship consent propagation under audit-grade discipline? Book a 30-minute call →

Share X LinkedIn
Build it yourself?

Get the kit, not just the theory.

We'll send the build checklist behind this post — and the next pillar when it ships. One email, no drip sequence. Unsubscribe in one click.

Want this built for you?

Book a discovery call. We'll walk your numbers.

20 minutes. Tell us what's broken, hear what we'd ship in the next 90 days. No pitch deck.