DEV Community

Alex Kane
Alex Kane

Posted on

n8n for AdTech & MarTech SaaS Vendors: 5 Automations for IAB TCF 2.2, CCPA, COPPA, and FTC Compliance

n8n for AdTech & MarTech SaaS Vendors: 5 Automations for IAB TCF 2.2, CCPA, COPPA, and FTC Compliance

Target audience: AdTech SaaS vendors building programmatic advertising infrastructure -- demand-side platforms (DSPs), supply-side platforms (SSPs), consent management platforms (CMPs), data management platforms (DMPs), ad attribution SaaS, retail media networks. Not marketing teams using ad tools -- the companies building them.

The IAB TCF 2.2 consent string your DSP processes is only valid if every vendor in the processing chain is a registered Global Vendor List entry. If your automation platform is not on that list and it touches a Purpose 1 consent signal, the entire consent string is invalid under TCF Policy section 5.3.2 -- and each invalid bid request is a potential GDPR Art.7 violation.

That is the specific problem with routing programmatic consent infrastructure through a cloud iPaaS that is not a registered GVL vendor.

Here are 5 n8n workflows with complete import-ready JSON that AdTech SaaS vendors use to manage consent signal integrity, CCPA opt-out propagation, COPPA under-13 protections, and FTC dark pattern compliance.


1. AdTech Client Tier-Segmented Onboarding Drip

Segments new customers across 7 tiers: DSP_PLATFORM_VENDOR, SSP_AD_EXCHANGE_VENDOR, CONSENT_MANAGEMENT_PLATFORM, DMP_DATA_ANALYTICS_VENDOR, AD_ATTRIBUTION_SAAS, RETAIL_MEDIA_NETWORK_SAAS, ADTECH_STARTUP.

Compliance flags set on intake: IAB_TCF_VENDOR_REGISTERED (checks if customer has a GVL vendor ID), CCPA_CPRA_CPPA_COVERED (>=100K US consumers), FTC_COPPA_RULE_SUBJECT (serves under-13 content), GDPR_ART6_CONSENT_REQUIRED (>=1K EU DAU), EU_EPRIVACY_DIRECTIVE_SUBJECT (sets EU cookies), FTC_ACT_SECTION5_SUBJECT (all), SOC2_REQUIRED (>=200K ARR).

Day 0 email is tier-specific: DSP vendors get IAB GVL registration audit + CCPA 15-day propagation + COPPA under-13 exclusion checklist. CMPs get GDPR Art.7(3) withdrawal mechanism requirements + CJEU Planet49 C-673/17 note. SSPs get EU ePrivacy Art.5(3) cookie consent chain audit + GDPR Art.28 DPA execution sequence.

{
  "name": "AdTech Client Tier-Segmented Onboarding Drip",
  "nodes": [
    {
      "id": "1",
      "name": "Google Sheets Trigger",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "parameters": {
        "sheetId": "your-sheet-id",
        "range": "Customers!A:Z",
        "event": "rowAdded"
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Classify AdTech Tier",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const r = $input.first().json;\nconst tier = (() => {\n  if (r.arr >= 5000000 || r.monthly_bid_requests >= 10000000000) return 'DSP_PLATFORM_VENDOR';\n  if (r.arr >= 1000000 || r.monthly_bid_requests >= 1000000000) return 'SSP_AD_EXCHANGE_VENDOR';\n  if (r.arr >= 500000 || r.daily_consent_events >= 10000000) return 'CONSENT_MANAGEMENT_PLATFORM';\n  if (r.arr >= 200000 || r.data_segments >= 1000000) return 'DMP_DATA_ANALYTICS_VENDOR';\n  if (r.arr >= 100000) return 'AD_ATTRIBUTION_SAAS';\n  if (r.arr >= 50000) return 'RETAIL_MEDIA_NETWORK_SAAS';\n  return 'ADTECH_STARTUP';\n})();\nconst flags = {\n  IAB_TCF_VENDOR_REGISTERED: r.iab_tcf_vendor_id ? true : false,\n  CCPA_CPRA_CPPA_COVERED: r.us_consumers_served >= 100000,\n  FTC_COPPA_RULE_SUBJECT: r.serves_under_13_content === true,\n  GDPR_ART6_CONSENT_REQUIRED: r.eu_dau >= 1000,\n  EU_EPRIVACY_DIRECTIVE_SUBJECT: r.sets_eu_cookies === true,\n  FTC_ACT_SECTION5_SUBJECT: true,\n  SOC2_REQUIRED: r.arr >= 200000\n};\nreturn [{json: {...r, tier, flags, onboarding_ts: new Date().toISOString()}}];"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Switch on Tier",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "dataType": "string",
        "value": "={{ $json.tier }}",
        "rules": {
          "values": [
            {
              "value": "DSP_PLATFORM_VENDOR"
            },
            {
              "value": "SSP_AD_EXCHANGE_VENDOR"
            },
            {
              "value": "CONSENT_MANAGEMENT_PLATFORM"
            },
            {
              "value": "DMP_DATA_ANALYTICS_VENDOR"
            },
            {
              "value": "AD_ATTRIBUTION_SAAS"
            },
            {
              "value": "RETAIL_MEDIA_NETWORK_SAAS"
            },
            {
              "value": "ADTECH_STARTUP"
            }
          ]
        }
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Day 0 Gmail",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.email }}",
        "subject": "Welcome to FlowKit -- Your AdTech Compliance Automation Setup",
        "message": "={{ 'Hi ' + $json.name + ',\\n\\nWelcome! Tier: ' + $json.tier + '\\n\\nKey compliance items:\\n- IAB TCF 2.2 GVL registration audit (Purpose 1 consent chain)\\n- CCPA 1798.135 opt-out propagation within 15 days\\n- COPPA under-13 audience exclusion verification\\n- GDPR Art.7 consent withdrawal propagation\\n- EU ePrivacy Art.5(3) cookie consent chain\\n\\nTemplates at stripeai.gumroad.com\\n\\nFlowKit Team' }}"
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack CSM Channel",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#adtech-csm",
        "text": "={{ 'New AdTech client: ' + $json.company + ' (' + $json.tier + ') ARR: $' + ($json.arr||0) + ' | Flags: ' + JSON.stringify($json.flags) }}"
      },
      "position": [
        1050,
        300
      ]
    },
    {
      "id": "6",
      "name": "Postgres Audit Log",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO adtech_onboarding_audit (company, email, tier, flags, arr, onboarding_ts, created_at) VALUES ('{{ $json.company }}', '{{ $json.email }}', '{{ $json.tier }}', '{{ JSON.stringify($json.flags) }}', {{ $json.arr || 0 }}, '{{ $json.onboarding_ts }}', NOW()) ON CONFLICT (email) DO UPDATE SET tier=EXCLUDED.tier, flags=EXCLUDED.flags, arr=EXCLUDED.arr;"
      },
      "position": [
        1250,
        300
      ]
    },
    {
      "id": "7",
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "unit": "days",
        "amount": 3
      },
      "position": [
        850,
        500
      ]
    },
    {
      "id": "8",
      "name": "Day 3 Follow-up",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.email }}",
        "subject": "FlowKit Day 3 -- IAB TCF 2.2 Consent Chain Audit Checklist",
        "message": "={{ 'Hi ' + $json.name + ',\\n\\nDay 3 check-in:\\n1. Is your iPaaS vendor on the IAB Global Vendor List? (Required if processing Purpose 1 consent)\\n2. CCPA opt-out: provable 15-day propagation with timestamps?\\n3. COPPA: bid filters exclude verified-under-13 inventory?\\n\\nFlowKit automates all three.\\n\\nstripeai.gumroad.com' }}"
      },
      "position": [
        1050,
        500
      ]
    },
    {
      "id": "9",
      "name": "Wait 4 More Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "unit": "days",
        "amount": 4
      },
      "position": [
        850,
        700
      ]
    },
    {
      "id": "10",
      "name": "Day 7 Gmail",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.email }}",
        "subject": "FlowKit Day 7 -- Your Compliance Automation Is Live",
        "message": "={{ 'Hi ' + $json.name + ',\\n\\nWeek 1 complete. Consent monitoring, CCPA opt-out tracker, and deadline tracker are running.\\n\\nNext: weekly AdTech KPI dashboard for CEO + DPO.\\n\\nstripeai.gumroad.com\\n\\nFlowKit Team' }}"
      },
      "position": [
        1050,
        700
      ]
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

2. IAB TCF 2.2 Consent Signal & CCPA Opt-Out Health Monitor

Polls consent infrastructure every 5 minutes. Detects three failure modes:

  • CRITICAL: Invalid consent strings detected in bid stream -- IAB TCF 2.2 section 5.3.2 + GDPR Art.7 -- each bid is potential EUR 20M fine or 4% global revenue
  • HIGH: Consent withdrawal propagation >3 seconds -- GDPR Art.7(3) CJEU C-673/17 Planet49 standard
  • STALE: Monitoring pipeline offline -- regulatory exposure active with no visibility

30-minute suppression window per endpoint prevents alert fatigue while maintaining compliance posture.

{
  "name": "IAB TCF 2.2 Consent Signal & CCPA Opt-Out Health Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Fetch Consent Status",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT e.endpoint_name, e.compliance_standard, e.blocking_risk, COUNT(*) FILTER (WHERE cs.valid = false AND cs.checked_at > NOW() - INTERVAL '5 minutes') as invalid_count, MAX(cs.checked_at) as last_check, AVG(EXTRACT(EPOCH FROM (cs.propagation_completed_at - cs.withdrawal_requested_at))) as avg_withdrawal_seconds FROM consent_endpoints e LEFT JOIN consent_signal_checks cs ON e.id = cs.endpoint_id GROUP BY e.endpoint_name, e.compliance_standard, e.blocking_risk;"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Classify Consent Health",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const checks = $input.all().map(i => i.json);\nconst state = $getWorkflowStaticData('global');\nif (!state.last_alert_ts) state.last_alert_ts = {};\nconst alerts = [];\nfor (const c of checks) {\n  const key = c.endpoint_name;\n  const now = Date.now();\n  const lastAlert = state.last_alert_ts[key] || 0;\n  if (now - lastAlert < 30 * 60 * 1000) continue;\n  let severity = null, regulation = '', action = '';\n  if (!c.last_check || (now - new Date(c.last_check).getTime()) > 15 * 60 * 1000) {\n    severity = 'STALE'; regulation = c.compliance_standard;\n    action = 'Consent monitoring offline -- regulatory exposure active';\n  } else if (c.invalid_count > 0 && c.blocking_risk === true) {\n    severity = 'CRITICAL'; regulation = 'IAB TCF 2.2 + GDPR Art.7';\n    action = c.invalid_count + ' invalid consent strings -- each bid = potential 20M EUR fine';\n  } else if (c.avg_withdrawal_seconds > 3 && c.compliance_standard && c.compliance_standard.includes('GDPR')) {\n    severity = 'HIGH'; regulation = 'GDPR Art.7(3) + EU ePrivacy Art.5(3)';\n    action = 'Consent withdrawal avg ' + Math.round(c.avg_withdrawal_seconds) + 's -- CJEU C-673/17 Planet49: must be immediate';\n  } else if (c.invalid_count > 0) {\n    severity = 'MEDIUM'; regulation = c.compliance_standard;\n    action = c.invalid_count + ' consent signal issues -- review before next bid cycle';\n  }\n  if (severity) {\n    state.last_alert_ts[key] = now;\n    alerts.push({json: {endpoint: key, severity, regulation, action, invalid_count: c.invalid_count, avg_withdrawal_s: c.avg_withdrawal_seconds, ts: new Date().toISOString()}});\n  }\n}\n$setWorkflowStaticData('global', state);\nreturn alerts.length ? alerts : [{json: {status: 'OK', checked: checks.length, ts: new Date().toISOString()}}];"
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "IF Alert",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.severity }}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack #consent-ops",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#consent-ops",
        "text": "={{ ($json.severity === 'CRITICAL' ? '\ud83d\udea8 CRITICAL' : '\u26a0\ufe0f ' + $json.severity) + ': ' + $json.endpoint + ' | ' + $json.regulation + ' | ' + $json.action }}"
      },
      "position": [
        1050,
        200
      ]
    },
    {
      "id": "6",
      "name": "Postgres Incident Log",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO consent_incidents (endpoint, severity, regulation, action, invalid_count, ts) VALUES ('{{ $json.endpoint }}', '{{ $json.severity }}', '{{ $json.regulation }}', '{{ $json.action }}', {{ $json.invalid_count || 0 }}, '{{ $json.ts }}') ON CONFLICT (endpoint, ts) DO NOTHING;"
      },
      "position": [
        1050,
        400
      ]
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

3. AdTech CCPA/GDPR/IAB TCF/COPPA/CAN-SPAM Compliance Deadline Tracker

Runs weekday mornings. Tracks 12 deadline types with 4-hour notification deduplication:

Deadline Type Regulation Critical Window
IAB_TCF_GVL_UPDATE IAB TCF 2.2 section 5.3.2 Continuous -- each GVL update
CCPA_OPT_OUT_PROPAGATION_AUDIT CCPA 1798.135 15 days from opt-out event
CCPA_DSR_45DAY CCPA 1798.105 45 days -- $7,500/intentional violation
COPPA_PARENTAL_CONSENT_AUDIT COPPA 16 CFR 312.5 Permanent retention required
GDPR_ART30_ROPA GDPR Art.30 Annual + any new processing
GDPR_DSR_30DAY GDPR Art.17/15 30 days from request
GDPR_DPA_REVIEW GDPR Art.28 Before any new sub-processor
EU_EPRIVACY_CONSENT_AUDIT EU ePrivacy Art.5(3) Annual + CJEU case monitoring
FTC_DARK_PATTERN_REVIEW FTC Section 5 Annual -- $50,674/violation/day
CAN_SPAM_SUPPRESSION_AUDIT CAN-SPAM 7704(a)(4) 10 business days from opt-out
SOC2_TYPE2_RENEWAL SOC 2 CC9.2 Annual
CASL_COMPLIANCE_ANNUAL CASL Section 6 Annual -- up to $10M/violation
{
  "name": "AdTech CCPA/GDPR/IAB TCF/COPPA/CAN-SPAM Compliance Deadline Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Weekdays 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Fetch Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "sheetId": "your-deadline-sheet-id",
        "range": "AdTechDeadlines!A:H"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Calculate Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst state = $getWorkflowStaticData('global');\nif (!state.last_notified) state.last_notified = {};\nconst actionMap = {\n  IAB_TCF_GVL_UPDATE: {owner: 'engineering', action: 'Check IAB Global Vendor List version -- update SDK if new version released (TCF Policy 5.3.2)', regulation: 'IAB TCF 2.2'},\n  CCPA_OPT_OUT_PROPAGATION_AUDIT: {owner: 'compliance', action: 'Audit CCPA 1798.135 opt-out propagation: verify all partners received signal within 15 days (CPPA enforcement)', regulation: 'CCPA 1798.135'},\n  CCPA_DSR_45DAY: {owner: 'compliance', action: 'CCPA 1798.105 data deletion request -- 45-day response window', regulation: 'CCPA 1798.105'},\n  COPPA_PARENTAL_CONSENT_AUDIT: {owner: 'compliance', action: 'FTC COPPA Rule 312.5 -- audit verifiable parental consent records. Permanent retention required.', regulation: 'COPPA 16 CFR 312'},\n  GDPR_ART30_ROPA: {owner: 'dpo', action: 'GDPR Art.30 Records of Processing -- update for any new ad tech partners or processing purposes', regulation: 'GDPR Art.30'},\n  GDPR_DSR_30DAY: {owner: 'dpo', action: 'GDPR Art.17 erasure / Art.15 access -- 30-day window', regulation: 'GDPR Art.17/15'},\n  GDPR_DPA_REVIEW: {owner: 'legal', action: 'GDPR Art.28 DPA -- any new iPaaS, cloud vendor, or ad partner requires executed DPA before processing EU data', regulation: 'GDPR Art.28'},\n  EU_EPRIVACY_CONSENT_AUDIT: {owner: 'engineering', action: 'EU ePrivacy Art.5(3): audit cookie consent. CJEU C-673/17 Planet49 -- pre-ticked boxes invalid. C-61/19 Orange Romania -- forced consent invalid.', regulation: 'EU ePrivacy Art.5(3)'},\n  FTC_DARK_PATTERN_REVIEW: {owner: 'ux', action: 'FTC 2022 Commercial Surveillance Report -- audit consent flows for dark patterns: pre-checked boxes, hidden opt-out, consent fatigue. $50,674/violation/day.', regulation: 'FTC Sec.5'},\n  CAN_SPAM_SUPPRESSION_AUDIT: {owner: 'engineering', action: 'CAN-SPAM 7704(a)(4) -- verify suppression list sync within 10 business days of opt-out. $51,744/violation.', regulation: 'CAN-SPAM 7704(a)(4)'},\n  SOC2_TYPE2_RENEWAL: {owner: 'infosec', action: 'SOC 2 Type II annual audit -- CC9.2: document all ad tech vendor relationships including iPaaS', regulation: 'SOC 2 CC9.2'},\n  CASL_COMPLIANCE_ANNUAL: {owner: 'compliance', action: 'CASL Sec.6 commercial electronic messages to Canadian recipients -- audit express consent records. CRTC up to $10M/violation.', regulation: 'CASL Sec.6'}\n};\nconst today = new Date();\nconst results = [];\nfor (const row of rows) {\n  const deadline = new Date(row.deadline_date);\n  const daysUntil = Math.ceil((deadline - today) / (1000 * 60 * 60 * 24));\n  const key = row.deadline_type + '_' + row.deadline_date;\n  const lastNotified = state.last_notified[key];\n  const hoursSince = lastNotified ? (Date.now() - new Date(lastNotified).getTime()) / 3600000 : 999;\n  if (hoursSince < 4) continue;\n  let severity = null;\n  if (daysUntil < 0) severity = 'OVERDUE';\n  else if (daysUntil <= 1) severity = 'CRITICAL';\n  else if (daysUntil <= 3) severity = 'URGENT';\n  else if (daysUntil <= 7) severity = 'WARNING';\n  else if (daysUntil <= 14) severity = 'NOTICE';\n  if (severity) {\n    const meta = actionMap[row.deadline_type] || {owner: 'compliance', action: row.notes, regulation: row.regulation};\n    state.last_notified[key] = new Date().toISOString();\n    results.push({json: {severity, deadline_type: row.deadline_type, deadline_date: row.deadline_date, days_until: daysUntil, owner: meta.owner, action: meta.action, regulation: meta.regulation, ts: new Date().toISOString()}});\n  }\n}\n$setWorkflowStaticData('global', state);\nreturn results.length ? results : [{json: {status: 'OK_NO_DEADLINES', checked: rows.length}}];"
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Switch Severity",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "dataType": "string",
        "value": "={{ $json.severity }}",
        "rules": {
          "values": [
            {
              "value": "OVERDUE"
            },
            {
              "value": "CRITICAL"
            },
            {
              "value": "URGENT"
            },
            {
              "value": "WARNING"
            },
            {
              "value": "NOTICE"
            }
          ]
        }
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack Urgent",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#adtech-compliance",
        "text": "={{ ($json.severity === 'OVERDUE' ? '\ud83d\udea8 OVERDUE' : '\ud83d\udd34 CRITICAL') + ': ' + $json.deadline_type + ' (' + $json.days_until + ' days) | ' + $json.regulation + ' | ' + $json.action + ' | owner: @' + $json.owner + ' <!here>' }}"
      },
      "position": [
        1050,
        200
      ]
    },
    {
      "id": "6",
      "name": "Gmail Owner",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.owner + '@yourcompany.com' }}",
        "subject": "={{ '[' + $json.severity + '] AdTech Compliance: ' + $json.deadline_type + ' -- ' + $json.days_until + ' days' }}",
        "message": "={{ 'Regulation: ' + $json.regulation + '\\nDeadline: ' + $json.deadline_date + ' (' + $json.days_until + ' days)\\nAction: ' + $json.action }}"
      },
      "position": [
        1050,
        400
      ]
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

4. AdTech Consent Violation & Dark Pattern Incident Alert Pipeline

Webhook-driven. Classifies 8 incident types with per-event 30-minute dedup and regulatory window assignment:

Incident Type Severity Window Penalty
IAB_TCF_INVALID_CONSENT_STRING CRITICAL IMMEDIATE EUR 20M / 4% global revenue
CCPA_OPT_OUT_PROPAGATION_MISSED CRITICAL 15 days $7,500/intentional + $100-750/consumer
COPPA_UNDERAGE_AD_SERVED CRITICAL IMMEDIATE $51,744/violation/child/day
EU_EPRIVACY_COOKIE_COMPLAINT HIGH 72h DPA response Per CJEU C-673/17
GDPR_CONSENT_WITHDRAWAL HIGH IMMEDIATE propagation GDPR Art.7(3)
FTC_DARK_PATTERN_COMPLAINT HIGH 30 days FTC response $50,674/violation/day
CAN_SPAM_UNSUBSCRIBE_FAILURE HIGH 10 business days $51,744/violation
CCPA_DATA_REQUEST_OVERDUE MEDIUM 45 days $7,500/intentional
{
  "name": "AdTech Consent Violation & Dark Pattern Incident Alert Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "adtech-incident",
        "method": "POST"
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Classify Incident",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const evt = $input.first().json;\nconst state = $getWorkflowStaticData('global');\nif (!state.seen) state.seen = {};\nconst key = evt.incident_type + '_' + (evt.consumer_id || evt.endpoint || evt.campaign_id || 'global');\nconst now = Date.now();\nif (state.seen[key] && now - state.seen[key] < 30 * 60 * 1000) return [{json: {suppressed: true, key}}];\nstate.seen[key] = now;\n$setWorkflowStaticData('global', state);\nconst incidents = {\n  IAB_TCF_INVALID_CONSENT_STRING: {severity: 'CRITICAL', window: 'IMMEDIATE', regulation: 'IAB TCF 2.2 + GDPR Art.7', action: 'Halt bid requests with invalid consent string. Audit CMP. Each = potential 20M EUR fine.'},\n  CCPA_OPT_OUT_PROPAGATION_MISSED: {severity: 'CRITICAL', window: '15 days from opt-out (CPPA)', regulation: 'CCPA 1798.135', action: 'Propagate opt-out to all demand partners now. Document timestamps. CPPA: $7,500/intentional + $100-750/consumer.'},\n  COPPA_UNDERAGE_AD_SERVED: {severity: 'CRITICAL', window: 'IMMEDIATE', regulation: 'COPPA 15 USC 6502 + FTC 16 CFR 312', action: 'Pull all ads from verified-under-13 inventory. $51,744/violation/child/day. Preserve bid logs.'},\n  EU_EPRIVACY_COOKIE_COMPLAINT: {severity: 'HIGH', window: '72h DPA response', regulation: 'EU ePrivacy Art.5(3) + GDPR Art.33', action: 'Document cookie consent chain. CJEU C-673/17 Planet49: pre-ticked consent invalid.'},\n  GDPR_CONSENT_WITHDRAWAL: {severity: 'HIGH', window: 'IMMEDIATE propagation', regulation: 'GDPR Art.7(3)', action: 'Propagate withdrawal across bid landscape. Document sub-second confirmation.'},\n  FTC_DARK_PATTERN_COMPLAINT: {severity: 'HIGH', window: '30 days FTC response', regulation: 'FTC Sec.5 + 2022 Commercial Surveillance Report', action: 'Preserve UI/UX consent flow screenshots. FTC: pre-checked boxes, consent fatigue = Sec.5 violation. $50,674/violation/day.'},\n  CAN_SPAM_UNSUBSCRIBE_FAILURE: {severity: 'HIGH', window: '10 business days', regulation: 'CAN-SPAM 7704(a)(4)', action: 'Process unsubscribe now. Document suppression list sync timestamp. $51,744/violation.'},\n  CCPA_DATA_REQUEST_OVERDUE: {severity: 'MEDIUM', window: '45 days from request', regulation: 'CCPA 1798.105', action: 'Process data deletion/access request. CPPA: $7,500/intentional violation.'}\n};\nconst meta = incidents[evt.incident_type] || {severity: 'MEDIUM', window: 'See compliance team', regulation: 'Unknown', action: 'Escalate to compliance'};\nreturn [{json: {...evt, ...meta, incident_id: key, ts: new Date().toISOString()}}];"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "IF Not Suppressed",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.suppressed }}",
              "value2": true,
              "operation": "notEqual"
            }
          ]
        }
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Slack #legal-adtech",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#legal-adtech",
        "text": "={{ ($json.severity === 'CRITICAL' ? '\ud83d\udea8 <!channel> CRITICAL' : '\u26a0\ufe0f HIGH') + ': ' + $json.incident_type + ' | ' + $json.regulation + ' | Window: ' + $json.window + ' | ' + $json.action }}"
      },
      "position": [
        850,
        200
      ]
    },
    {
      "id": "5",
      "name": "Postgres Log",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO adtech_incidents (incident_type, severity, regulation, window_str, action_required, ts) VALUES ('{{ $json.incident_type }}', '{{ $json.severity }}', '{{ $json.regulation }}', '{{ $json.window }}', '{{ $json.action }}', '{{ $json.ts }}') ON CONFLICT (incident_type, ts) DO NOTHING;"
      },
      "position": [
        850,
        400
      ]
    },
    {
      "id": "6",
      "name": "Respond 200 ACK",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "responseCode": 200,
        "responseBody": "={{ JSON.stringify({received: true, incident_id: $json.incident_id, severity: $json.severity, window: $json.window, ts: $json.ts}) }}"
      },
      "position": [
        1050,
        300
      ]
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

5. Weekly AdTech Platform Compliance KPI Dashboard

Runs Monday 8 AM. Pulls platform metrics and incident summary from two parallel Postgres queries, merges, and sends HTML email to CEO with BCC to DPO and legal.

Dashboard: customers by tier, ARR WoW% via $getWorkflowStaticData, consent opt-in rate (color-coded: green >=75% / orange >=60% / red <60%), TCF GVL registration count, COPPA-subject count, 7-day incident breakdown (CRITICAL / HIGH / COPPA / CCPA / TCF).

{
  "name": "Weekly AdTech Platform Compliance KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Monday 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Fetch Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT tier, COUNT(*) as customers, SUM(arr) as tier_arr, AVG(consent_opt_in_rate) as avg_consent_rate, COUNT(*) FILTER (WHERE flags->>'IAB_TCF_VENDOR_REGISTERED' = 'true') as tcf_registered, COUNT(*) FILTER (WHERE flags->>'FTC_COPPA_RULE_SUBJECT' = 'true') as coppa_subject FROM adtech_customers GROUP BY tier ORDER BY tier_arr DESC;"
      },
      "position": [
        450,
        200
      ]
    },
    {
      "id": "3",
      "name": "Fetch Incident Summary",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) FILTER (WHERE severity='CRITICAL' AND ts > NOW() - INTERVAL '7 days') as critical_7d, COUNT(*) FILTER (WHERE severity='HIGH' AND ts > NOW() - INTERVAL '7 days') as high_7d, COUNT(*) FILTER (WHERE incident_type='COPPA_UNDERAGE_AD_SERVED') as coppa_incidents, COUNT(*) FILTER (WHERE incident_type LIKE '%CCPA%' AND ts > NOW() - INTERVAL '7 days') as ccpa_7d, COUNT(*) FILTER (WHERE incident_type LIKE '%TCF%' AND ts > NOW() - INTERVAL '7 days') as tcf_7d FROM adtech_incidents;"
      },
      "position": [
        450,
        500
      ]
    },
    {
      "id": "4",
      "name": "Merge Results",
      "type": "n8n-nodes-base.merge",
      "parameters": {
        "mode": "multiplex"
      },
      "position": [
        650,
        350
      ]
    },
    {
      "id": "5",
      "name": "Build KPI Dashboard",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const metrics = $input.all().filter(i => i.json.tier).map(i => i.json);\nconst inc = $input.all().find(i => i.json.critical_7d !== undefined)?.json || {};\nconst state = $getWorkflowStaticData('global');\nconst lastMetrics = state.last_metrics || {};\nstate.last_metrics = {};\nmetrics.forEach(m => { state.last_metrics[m.tier] = {arr: m.tier_arr}; });\n$setWorkflowStaticData('global', state);\nconst totalArr = metrics.reduce((s, m) => s + parseFloat(m.tier_arr || 0), 0);\nconst lastArr = Object.values(lastMetrics).reduce((s, m) => s + parseFloat(m.arr || 0), 0);\nconst wow = lastArr > 0 ? ((totalArr - lastArr) / lastArr * 100).toFixed(1) : 'N/A';\nconst arrColor = wow === 'N/A' ? '#888' : parseFloat(wow) >= 5 ? '#27ae60' : parseFloat(wow) >= 0 ? '#f39c12' : '#e74c3c';\nconst tierRows = metrics.map(m => {\n  const rate = parseFloat(m.avg_consent_rate || 0);\n  const rateColor = rate >= 0.75 ? '#27ae60' : rate >= 0.60 ? '#f39c12' : '#e74c3c';\n  return '<tr><td>' + m.tier + '</td><td>' + m.customers + '</td><td>$' + parseFloat(m.tier_arr||0).toLocaleString() + '</td><td><span style=\"color:' + rateColor + '\">' + (rate*100).toFixed(1) + '%</span></td><td>' + m.tcf_registered + '</td><td>' + m.coppa_subject + '</td></tr>';\n}).join('');\nconst html = '<h2>FlowKit AdTech Weekly KPI -- ' + new Date().toDateString() + '</h2>'\n  + '<table border=\"1\" cellpadding=\"6\"><tr><th>Tier</th><th>Customers</th><th>ARR</th><th>Consent Rate</th><th>TCF Registered</th><th>COPPA Subject</th></tr>'\n  + tierRows + '</table><br>'\n  + '<b>Total ARR:</b> $' + totalArr.toLocaleString() + ' <span style=\"color:' + arrColor + '\">WoW: ' + wow + '%</span><br>'\n  + '<b>Incidents (7d):</b> Critical: ' + (inc.critical_7d||0) + ' High: ' + (inc.high_7d||0)\n  + ' COPPA: ' + (inc.coppa_incidents||0) + ' CCPA: ' + (inc.ccpa_7d||0) + ' TCF: ' + (inc.tcf_7d||0);\nreturn [{json: {html, total_arr: totalArr, wow, ts: new Date().toISOString()}}];"
      },
      "position": [
        850,
        350
      ]
    },
    {
      "id": "6",
      "name": "Gmail CEO BCC DPO",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "ceo@yourcompany.com",
        "bcc": "dpo@yourcompany.com, legal@yourcompany.com",
        "subject": "={{ 'AdTech Weekly KPI -- ' + new Date().toDateString() }}",
        "message": "={{ $json.html }}",
        "options": {
          "isBodyHtml": true
        }
      },
      "position": [
        1050,
        250
      ]
    },
    {
      "id": "7",
      "name": "Slack #exec-kpis",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#exec-adtech-kpis",
        "text": "={{ 'AdTech Weekly KPI: $' + $json.total_arr.toLocaleString() + ' ARR (WoW: ' + $json.wow + '%) -- see email for full dashboard' }}"
      },
      "position": [
        1050,
        450
      ]
    }
  ],
  "connections": {}
}
Enter fullscreen mode Exit fullscreen mode

Why Self-Hosted n8n -- the AdTech-Specific Arguments

IAB TCF 2.2 GVL Registration (section 5.3.2)
TCF Policy requires every vendor processing consent signals under Purpose 1 to be a registered Global Vendor List entry. A cloud iPaaS is not on the GVL. If it processes consent signal metadata, validation requests, or audit log writes as part of your TCF consent chain, you have an unregistered vendor in your consent architecture. This invalidates the consent string.

CCPA 1798.135 15-Day Opt-Out Propagation
CPRA amendment to CCPA and CPPA enforcement regulations require opt-out of sale/sharing signals to propagate to all third parties within 15 days. If your iPaaS handles opt-out signal routing and has a silent webhook failure, the clock ran from the consumer's request date. CPPA: $7,500 per intentional violation plus $100-750 per consumer per incident.

COPPA 16 CFR 312 -- $51,744/Violation/Child/Day
FTC COPPA Rule section 312.5 requires verifiable parental consent records to be retained permanently. If bid filtering or age-gate compliance logic routes through a cloud iPaaS, that vendor holds the evidence record. A cloud iPaaS with 30-day log retention means the FTC audit trail gap starts on day 31.

FTC Section 5 Dark Patterns -- 2022 Commercial Surveillance Report
The FTC's 2022 Report on Commercial Surveillance and Data Security identified dark patterns in consent flows -- pre-checked consent boxes, consent fatigue design, and hidden opt-out -- as potential section 5 unfair or deceptive trade practice violations. $50,674/violation/day. Cloud iPaaS holding consent UI audit logs is an independent document production target.

EU ePrivacy Art.5(3) -- CJEU Planet49 Standard
CJEU C-673/17 Planet49 (2019): pre-ticked consent checkbox for cookies = invalid consent. CJEU C-61/19 Orange Romania (2020): consent obtained under terms of service = invalid. Each non-consented cookie placement is a separate infraction. At programmatic scale (1B+ bid requests/day), regulatory sample-based enforcement reaches significant fines before individual complaints.

CAN-SPAM 7704(a)(4) Suppression Propagation
Unsubscribe mechanism must process opt-outs within 10 business days. If suppression list sync routes through a cloud iPaaS that fails silently, the 10-day window starts from the complaint date, not the re-sync date. $51,744/violation.

Self-hosted n8n architecture note: n8n is not a registered GVL vendor -- keep it out of the live bid request path. It safely manages monitoring and alerting workflows outside the consent chain. COPPA audit trail records stay in your Postgres instance under your retention policy. Cost: 10B bid events/month on a cloud iPaaS = $100K+. VPS: $500/month.


Get the Complete Workflow Pack

All 5 workflows are available as import-ready JSON at stripeai.gumroad.com -- individual templates from $12, complete bundle $97.

The bundle includes the AdTech compliance pack plus 14 other automation templates: customer onboarding drips, API health monitors, compliance deadline trackers, incident alert pipelines, and weekly KPI dashboards across 15 verticals.


FlowKit builds n8n automation templates for SaaS vendors. All workflow JSON is import-ready -- paste into your n8n instance, connect your Postgres, Slack, and Gmail credentials, and configure your sheet IDs.

Top comments (0)