This workflow corresponds to n8n.io template #17191 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow reads backend conversion events, normalizes and SHA-256-hashes required identifiers, and then sends each conversion to Meta Conversions API, Google Ads Enhanced Conversions, or both using HTTP requests with environment-based access tokens. Runs manually to start…
Source: https://n8n.io/workflows/17191/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n workflow allows you to interact with Binance Spot Trading API directly to: Place Limit Buy and Limit Sell orders Place Market Buy and Market Sell orders Query account info and open orders Can
If you're using Meta Ads to generate new leads to your sales pipeline, this workflow is for you! 🙌🏻 Triggers every time you have a new calendar event on a chosen Google Acount Filter only events with
Uploading an image to Webflow's Asset Manager through the API is surprisingly complex. It requires computing an MD5 hash, announcing the file to Webflow, then uploading the binary to Amazon S3 with sp
This workflow listens for an “Approved” label on a Trello card, reads the AI draft bookkeeping JSON from card comments, and posts the corresponding transaction to Xero. It then adds a Xero deep link b
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.