{
  "id": "1d0fb4f7f88d885e",
  "name": "Send backend conversions to Meta Conversions API and Google Ads",
  "tags": [],
  "nodes": [
    {
      "id": "overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -736,
        32
      ],
      "parameters": {
        "width": 672,
        "height": 896,
        "content": "## Send backend conversions to Meta and Google\n\nSend server-side (backend) conversions to Meta and Google Ads from one workflow. It reads conversions from your source, hashes the identifiers that require it, and routes each to the right platform.\n\n### How it works\n- Reads backend conversions from your source (example rows included; swap in Supabase, Postgres or Google Sheets).\n- Hashes only the PII the platforms require (email, phone, name, city, state, zip, country) in one loop through the native Crypto node (SHA-256). external_id and device or click IDs (fbc, fbp, IP, user agent) pass through unhashed, per the Meta and Google specs.\n- Regroups the hashed and plain fields per conversion.\n- A source switch routes by destination tag: Meta to the Meta Conversions API, Google to Google Ads, both to both.\n\n### Setup\n1. Edit the Set config node: Meta pixel ID, Google Ads customer ID, conversion action, versions. Set test_event_code to test via Meta Test Events.\n2. Add secret tokens to your n8n env: META_ACCESS_TOKEN, GOOGLE_ADS_ACCESS_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN.\n3. Replace the example source node with your data.\n\n### Customization\nOnly ever send first-party, consented data.\n\nBuilt by **nocode.expert** \u2014 done-for-you automation & tracking. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "section-source-normalize",
      "name": "Section: Source & normalize",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 508,
        "height": 416,
        "content": "## 1. Config, source & normalize\nSet your ids in the config node, read conversions, and expand each into its hashable identifier fields."
      },
      "typeVersion": 1
    },
    {
      "id": "section-hash-sha-256",
      "name": "Section: Hash SHA-256",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 492,
        "height": 416,
        "content": "## 2. Hash (loop, SHA-256)\nLoop ONLY the fields the platforms require hashed through the native Crypto node. external_id and device/click IDs stay plain. Then regroup per conversion."
      },
      "typeVersion": 1
    },
    {
      "id": "section-send-to-meta-google",
      "name": "Section: Send to Meta + Google",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1344,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 604,
        "height": 624,
        "content": "## 3. Route & send\nA source switch sends each conversion to Meta, Google, or both \u2014 only the right data reaches each platform."
      },
      "typeVersion": 1
    },
    {
      "id": "run-manually",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        64,
        368
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "set-config-ids-versions",
      "name": "Set config (ids & versions)",
      "type": "n8n-nodes-base.set",
      "position": [
        288,
        368
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "META_PIXEL_ID",
              "name": "META_PIXEL_ID",
              "type": "string",
              "value": "YOUR_PIXEL_ID"
            },
            {
              "id": "META_API_VERSION",
              "name": "META_API_VERSION",
              "type": "string",
              "value": "v21.0"
            },
            {
              "id": "META_TEST_EVENT_CODE",
              "name": "META_TEST_EVENT_CODE",
              "type": "string",
              "value": ""
            },
            {
              "id": "GOOGLE_ADS_CUSTOMER_ID",
              "name": "GOOGLE_ADS_CUSTOMER_ID",
              "type": "string",
              "value": "YOUR_CUSTOMER_ID"
            },
            {
              "id": "GOOGLE_ADS_LOGIN_CUSTOMER_ID",
              "name": "GOOGLE_ADS_LOGIN_CUSTOMER_ID",
              "type": "string",
              "value": ""
            },
            {
              "id": "GOOGLE_ADS_CONVERSION_ACTION",
              "name": "GOOGLE_ADS_CONVERSION_ACTION",
              "type": "string",
              "value": "customers/YOUR_CUSTOMER_ID/conversionActions/YOUR_ACTION_ID"
            },
            {
              "id": "GOOGLE_ADS_API_VERSION",
              "name": "GOOGLE_ADS_API_VERSION",
              "type": "string",
              "value": "v18"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "read-backend-conversions-example-rows",
      "name": "Read backend conversions (example rows)",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        368
      ],
      "parameters": {
        "jsCode": "// SOURCE OF TRUTH \u2014 replace with your Supabase / Postgres / Google Sheets node.\n// One item per conversion, tagged destination: meta | google | both.\n// Hashed: email, phone, name, city, state, zip, country. Plain: external_id + device/click IDs.\n// ONLY first-party consented data. Never append PII from third-party sources.\nreturn [\n  { json: { destination: 'both',   email: 'user@example.com ', phone: '+1234567890', first_name: 'Jane', last_name: 'Doe', city: 'San Francisco', state: 'CA', zip: '94103', country: 'US', external_id: 'crm_1001', client_ip_address: '203.0.113.10', client_user_agent: 'Mozilla/5.0', fbc: '', fbp: '', value: 2400, currency: 'USD', event_name: 'Purchase', event_time: 0, event_id: 'conv_1001' } },\n  { json: { destination: 'meta',   email: 'user@example.com',           phone: '4155550111',        first_name: 'Sam',  last_name: 'Lee', city: 'Austin',        state: 'TX', zip: '73301', country: 'US', external_id: 'crm_1002', client_ip_address: '203.0.113.11', client_user_agent: 'Mozilla/5.0', fbc: '', fbp: '', value:  900, currency: 'USD', event_name: 'Purchase', event_time: 0, event_id: 'conv_1002' } },\n  { json: { destination: 'google', email: 'user@example.com',            phone: '4155550122',        first_name: 'Ava',  last_name: 'Ng',  city: 'Denver',        state: 'CO', zip: '80202', country: 'US', external_id: 'crm_1003', client_ip_address: '203.0.113.12', client_user_agent: 'Mozilla/5.0', fbc: '', fbp: '', value: 1500, currency: 'USD', event_name: 'Purchase', event_time: 0, event_id: 'conv_1003' } },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "expand-normalize-identifiers",
      "name": "Expand & normalize identifiers",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        368
      ],
      "parameters": {
        "jsCode": "const norm = (k, v) => {\n  v = (v == null ? '' : String(v)).trim();\n  if (!v) return '';\n  if (k === 'ph') return v.replace(/[^0-9]/g, '');\n  if (k === 'country') return v.toLowerCase().slice(0, 2);\n  if (k === 'zp') return v.toLowerCase().replace(/[\\s-]/g, '');\n  return v.toLowerCase().replace(/\\s+/g, '');\n};\nconst FIELDS = [['email','em'],['phone','ph'],['first_name','fn'],['last_name','ln'],['city','ct'],['state','st'],['zip','zp'],['country','country']];\nconst PLAIN_META = ['external_id','fbc','fbp','client_ip_address','client_user_agent'];\n// Expand each conversion into one item per HASHABLE identifier, normalized. The Crypto\n// node then loops over all of these. external_id + device/click IDs are NOT expanded here.\nconst deals = $input.all().map((i) => i.json);\nconst out = [];\ndeals.forEach((d, idx) => {\n  for (const [src, key] of FIELDS) {\n    const val = norm(key, d[src]);\n    if (val) out.push({ json: { dealIdx: idx, fieldKey: key, toHash: val } });\n  }\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "hash-identifier-sha-256",
      "name": "Hash identifier (SHA-256)",
      "type": "n8n-nodes-base.crypto",
      "position": [
        912,
        368
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.toHash }}",
        "dataPropertyName": "hashedVal"
      },
      "typeVersion": 1
    },
    {
      "id": "regroup-hashed-identifiers-per-conversion",
      "name": "Regroup hashed identifiers per conversion",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        368
      ],
      "parameters": {
        "jsCode": "const norm = (k, v) => {\n  v = (v == null ? '' : String(v)).trim();\n  if (!v) return '';\n  if (k === 'ph') return v.replace(/[^0-9]/g, '');\n  if (k === 'country') return v.toLowerCase().slice(0, 2);\n  if (k === 'zp') return v.toLowerCase().replace(/[\\s-]/g, '');\n  return v.toLowerCase().replace(/\\s+/g, '');\n};\nconst FIELDS = [['email','em'],['phone','ph'],['first_name','fn'],['last_name','ln'],['city','ct'],['state','st'],['zip','zp'],['country','country']];\nconst PLAIN_META = ['external_id','fbc','fbp','client_ip_address','client_user_agent'];\n// Collapse hashed items back per conversion. hashed = SHA-256 PII; plainAddr = unhashed\n// city/state/zip/country for Google; plainMeta = external_id + device/click IDs (unhashed).\nconst hashedItems = $('Hash identifier (SHA-256)').all().map((i) => i.json);\nconst deals = $('Read backend conversions (example rows)').all().map((i) => i.json);\nconst byDeal = {};\nfor (const h of hashedItems) { (byDeal[h.dealIdx] = byDeal[h.dealIdx] || {})[h.fieldKey] = h.hashedVal; }\nreturn deals.map((d, idx) => {\n  const plainMeta = {};\n  for (const k of PLAIN_META) { const v = (d[k] == null ? '' : String(d[k]).trim()); if (v) plainMeta[k] = v; }\n  return { json: {\n    destination: (d.destination || 'both'),\n    value: Number(d.value), currency: d.currency || 'USD',\n    event_name: d.event_name || 'Purchase', event_time: Number(d.event_time) || 0, event_id: d.event_id || ('conv_' + idx),\n    hashed: byDeal[idx] || {},\n    plainAddr: { city: norm('ct', d.city), state: norm('st', d.state), zip: norm('zp', d.zip), country: norm('country', d.country) },\n    plainMeta,\n  } };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "route-to-meta-skip-google-only",
      "name": "Route to Meta (skip google-only)",
      "type": "n8n-nodes-base.filter",
      "position": [
        1408,
        272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "route",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.destination }}",
              "rightValue": "google"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "build-meta-capi-payload",
      "name": "Build Meta CAPI payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        272
      ],
      "parameters": {
        "jsCode": "// Build the Meta Conversions API batch. Config comes from the Set config node (read via\n// the node reference below), so this runs cleanly under n8n's task runner. Set\n// test_event_code in config to test via Meta Test Events safely.\nconst cfg = $('Set config (ids & versions)').first().json;\nconst V = cfg.META_API_VERSION || 'v21.0';\nconst PIX = (cfg.META_PIXEL_ID || '').trim();\nconst now = Math.floor(Date.now() / 1000);\nconst rows = $input.all().map((i) => i.json).filter((d) => d.value != null);\nconst data = rows.map((d) => {\n  const ud = {};\n  for (const [k, v] of Object.entries(d.hashed)) ud[k] = [v];\n  const pm = d.plainMeta || {};\n  if (pm.external_id) ud.external_id = [pm.external_id];\n  if (pm.fbc) ud.fbc = pm.fbc;\n  if (pm.fbp) ud.fbp = pm.fbp;\n  if (pm.client_ip_address) ud.client_ip_address = pm.client_ip_address;\n  if (pm.client_user_agent) ud.client_user_agent = pm.client_user_agent;\n  return { event_name: d.event_name, event_time: d.event_time > 0 ? d.event_time : now, action_source: 'system_generated', event_id: d.event_id, user_data: ud, custom_data: { value: Number(d.value), currency: d.currency } };\n});\nconst payload = { data };\nif (cfg.META_TEST_EVENT_CODE) payload.test_event_code = cfg.META_TEST_EVENT_CODE;\nreturn [{ json: { url: `https://graph.facebook.com/${V}/${PIX}/events`, body: payload, count: data.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "send-to-meta-conversions-api",
      "name": "Send to Meta Conversions API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1792,
        272
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.body }}",
        "sendBody": true,
        "sendQuery": true,
        "specifyBody": "json",
        "queryParameters": {
          "parameters": [
            {
              "name": "access_token",
              "value": "={{ $env.META_ACCESS_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "route-to-google-skip-meta-only",
      "name": "Route to Google (skip meta-only)",
      "type": "n8n-nodes-base.filter",
      "position": [
        1408,
        464
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "route",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.destination }}",
              "rightValue": "meta"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "build-google-ads-payload",
      "name": "Build Google Ads payload",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        464
      ],
      "parameters": {
        "jsCode": "// Build the Google Ads Enhanced Conversions upload. Config from the Set config node reference.\nconst cfg = $('Set config (ids & versions)').first().json;\nconst GV = cfg.GOOGLE_ADS_API_VERSION || 'v18';\nconst CID = (cfg.GOOGLE_ADS_CUSTOMER_ID || '').replace(/-/g, '');\nconst CA = cfg.GOOGLE_ADS_CONVERSION_ACTION;\nconst iso = (t) => new Date((Number(t) > 0 ? Number(t) * 1000 : Date.now())).toISOString().replace('T', ' ').replace(/\\..*/, '+00:00');\nconst rows = $input.all().map((i) => i.json).filter((d) => d.value != null);\nconst conversions = rows.map((d) => {\n  const ui = [];\n  if (d.hashed.em) ui.push({ hashedEmail: d.hashed.em });\n  if (d.hashed.ph) ui.push({ hashedPhoneNumber: d.hashed.ph });\n  const a = d.plainAddr || {}; const addr = {};\n  if (d.hashed.fn) addr.hashedFirstName = d.hashed.fn;\n  if (d.hashed.ln) addr.hashedLastName = d.hashed.ln;\n  if (a.city) addr.city = a.city;\n  if (a.state) addr.state = a.state;\n  if (a.zip) addr.postalCode = a.zip;\n  if (a.country) addr.countryCode = a.country.toUpperCase();\n  if (Object.keys(addr).length) ui.push({ addressInfo: addr });\n  return { conversionAction: CA, conversionDateTime: iso(d.event_time), conversionValue: Number(d.value), currencyCode: d.currency, userIdentifiers: ui };\n});\nreturn [{ json: { url: `https://googleads.googleapis.com/${GV}/customers/${CID}:uploadClickConversions`, body: { conversions, partialFailure: true }, count: conversions.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "upload-to-google-ads-enhanced-conversions",
      "name": "Upload to Google Ads Enhanced Conversions",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1792,
        464
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.body }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GOOGLE_ADS_ACCESS_TOKEN }}"
            },
            {
              "name": "developer-token",
              "value": "={{ $env.GOOGLE_ADS_DEVELOPER_TOKEN }}"
            },
            {
              "name": "login-customer-id",
              "value": "={{ ($('Set config (ids & versions)').first().json.GOOGLE_ADS_LOGIN_CUSTOMER_ID || $('Set config (ids & versions)').first().json.GOOGLE_ADS_CUSTOMER_ID || '').replace(/-/g,'') }}"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "b3a9810a-1743-486b-8a2b-698c11bca4f6",
  "nodeGroups": [],
  "connections": {
    "Run manually": {
      "main": [
        [
          {
            "node": "Set config (ids & versions)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Meta CAPI payload": {
      "main": [
        [
          {
            "node": "Send to Meta Conversions API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Google Ads payload": {
      "main": [
        [
          {
            "node": "Upload to Google Ads Enhanced Conversions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash identifier (SHA-256)": {
      "main": [
        [
          {
            "node": "Regroup hashed identifiers per conversion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config (ids & versions)": {
      "main": [
        [
          {
            "node": "Read backend conversions (example rows)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand & normalize identifiers": {
      "main": [
        [
          {
            "node": "Hash identifier (SHA-256)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Google (skip meta-only)": {
      "main": [
        [
          {
            "node": "Build Google Ads payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Meta (skip google-only)": {
      "main": [
        [
          {
            "node": "Build Meta CAPI payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read backend conversions (example rows)": {
      "main": [
        [
          {
            "node": "Expand & normalize identifiers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Regroup hashed identifiers per conversion": {
      "main": [
        [
          {
            "node": "Route to Meta (skip google-only)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Route to Google (skip meta-only)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}