This workflow corresponds to n8n.io template #17104 — 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 →
{
"name": "Upload qualified lead conversions from Bitrix24 to Google Ads",
"tags": [],
"nodes": [
{
"id": "20defe67-a5fd-4564-b5a8-807bf2d1ad81",
"name": "Sticky Note bfb692",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1400,
-946
],
"parameters": {
"color": 1,
"width": 900,
"height": 786,
"content": "## Upload qualified lead conversions from Bitrix24 to Google Ads\n\nSmart Bidding only works when Google Ads knows which clicks became *real* leads.\nThis workflow closes that loop: when a deal is updated in Bitrix24, it checks\nwhether the deal is qualified, then uploads an offline click conversion with the\nstored GCLID plus hashed first-party customer data.\n\n### How it works\n1. A Bitrix24 outbound webhook fires on deal update.\n2. The deal is loaded through the Bitrix24 REST API.\n3. **Check Eligibility** applies four guards: a GCLID must exist, the deal must be\n in your qualified stage, it must not have been sent before, and a 2-minute\n dedupe blocks the duplicate events Bitrix24 likes to send.\n4. The linked contact is fetched; email, phone and name are normalised to Google's\n rules and hashed with SHA-256 by four Crypto nodes.\n5. The conversion is uploaded to `uploadClickConversions`.\n6. The outcome is written to the deal timeline and a flag field is set, so the\n same deal is never uploaded twice.\n\n### Setup\n1. Open the **Settings** node and fill in every value: Bitrix24 inbound webhook\n base URL, Google Ads customer ID, login customer ID (MCC), developer token,\n conversion action ID, and the three Bitrix24 custom field IDs (GCLID,\n qualified marker, sent flag).\n2. Connect a **Google Ads OAuth2** credential on the upload node.\n3. In Bitrix24, create an outbound webhook for `ONCRMDEALUPDATE` pointing at this\n workflow's production URL.\n4. Make sure your landing pages store the GCLID into the deal's GCLID field.\n\n### Customization tips\n- Only **Check Eligibility** and **Normalize Customer Data** are CRM-specific \u2014\n swap the Bitrix24 nodes for any CRM with a REST API.\n- To upload purchases, point `conversionActionId` at a purchase action."
},
"typeVersion": 1
},
{
"id": "69fbddd9-c781-4d77-8548-21fe03640983",
"name": "Sticky Note e031c0",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1400,
-100
],
"parameters": {
"color": 7,
"width": 1060,
"height": 310,
"content": "## 1. Receive the deal\nBitrix24 fires an outbound webhook on deal update; the deal is loaded over REST."
},
"typeVersion": 1
},
{
"id": "e522d8c6-5a7b-4f11-af1e-7cd376120f2f",
"name": "Sticky Note 413277",
"type": "n8n-nodes-base.stickyNote",
"position": [
-280,
-100
],
"parameters": {
"color": 7,
"width": 580,
"height": 310,
"content": "## 2. Decide whether to send\nGuards: GCLID present, deal qualified, not already sent, not a duplicate event."
},
"typeVersion": 1
},
{
"id": "e9968936-f5ab-42c5-88a1-2dc88d2f95f0",
"name": "Sticky Note 6555bb",
"type": "n8n-nodes-base.stickyNote",
"position": [
360,
-100
],
"parameters": {
"color": 7,
"width": 1540,
"height": 310,
"content": "## 3. Hash customer data\nEmail, phone and name are normalised to Google's rules, then hashed with SHA-256."
},
"typeVersion": 1
},
{
"id": "7f728ac4-e741-49f0-863e-343067f02c48",
"name": "Sticky Note f41871",
"type": "n8n-nodes-base.stickyNote",
"position": [
1960,
-100
],
"parameters": {
"color": 7,
"width": 1300,
"height": 510,
"content": "## 4. Upload and log\nUpload the click conversion, then write the outcome back to the deal timeline."
},
"typeVersion": 1
},
{
"id": "e5fb942c-a850-460d-8e38-cf966eca6d0e",
"name": "Sticky Note 2c5189",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1330,
270
],
"parameters": {
"color": 3,
"width": 460,
"height": 236,
"content": "## Fill this in first\n\nEvery value here is a placeholder. The workflow will fail until you replace them\nwith your own IDs.\n\nNever paste your Google Ads developer token into a public template or a shared\nworkflow export."
},
"typeVersion": 1
},
{
"id": "ee96a2b7-e62d-4455-999d-67343d8e6a74",
"name": "Bitrix24 Deal Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1330,
20
],
"parameters": {
"path": "bitrix24-qualified-lead",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "144ac003-136f-423e-97f8-19df42b9454b",
"name": "Settings",
"type": "n8n-nodes-base.set",
"position": [
-1090,
20
],
"parameters": {
"mode": "manual",
"options": {},
"assignments": {
"assignments": [
{
"id": "6bdf469e-8140-49e9-b36f-81b8dad22b25",
"name": "bitrixWebhookBase",
"type": "string",
"value": "https://YOUR-PORTAL.bitrix24.com/rest/1/YOUR_INBOUND_WEBHOOK_TOKEN"
},
{
"id": "57c1f5f6-4a30-47ed-87e7-672b9d1fb452",
"name": "googleAdsCustomerId",
"type": "string",
"value": "1234567890"
},
{
"id": "45239df6-d330-477f-ad19-051b11585ff7",
"name": "googleAdsLoginCustomerId",
"type": "string",
"value": "1234567890"
},
{
"id": "e81734ee-cf90-4eed-b26a-ec011a5654a2",
"name": "googleAdsDeveloperToken",
"type": "string",
"value": "YOUR_GOOGLE_ADS_DEVELOPER_TOKEN"
},
{
"id": "76bb9fdf-b5ad-4a65-955d-3bc619f6781f",
"name": "googleAdsApiVersion",
"type": "string",
"value": "v24"
},
{
"id": "9bc132de-bb43-421e-b595-2ca11450b54c",
"name": "conversionActionId",
"type": "string",
"value": "1234567890"
},
{
"id": "c4484607-64ec-45ef-bce1-d296cdab1484",
"name": "gclidFieldId",
"type": "string",
"value": "UF_CRM_+1234567890"
},
{
"id": "c2acd68c-3116-4956-89d1-84931bd0a70e",
"name": "qualifiedFieldId",
"type": "string",
"value": "UF_CRM_+1234567890"
},
{
"id": "ff11af12-56e0-4693-a72c-b06813253608",
"name": "qualifiedValue",
"type": "string",
"value": "100"
},
{
"id": "8fe9c6fe-aed9-418b-ada4-3ac272bc7ac5",
"name": "sentFlagFieldId",
"type": "string",
"value": "UF_CRM_+1234567890"
},
{
"id": "d471bda3-736b-44ca-b3bf-a0cf21fc6ec4",
"name": "fallbackCurrency",
"type": "string",
"value": "USD"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "351a73ed-e3db-4877-8755-501eb363bb70",
"name": "Extract Deal ID",
"type": "n8n-nodes-base.code",
"position": [
-850,
20
],
"parameters": {
"jsCode": "// Bitrix24 sends the deal ID in a few different shapes depending on how the\n// outbound webhook was configured, so try all of them.\nconst body = $input.first().json.body || $input.first().json || {};\n\nconst dealId =\n (body.data && body.data.FIELDS && body.data.FIELDS.ID) ||\n (body.FIELDS && body.FIELDS.ID) ||\n body['data[FIELDS][ID]'] ||\n body.dealId ||\n body.ID ||\n '';\n\nif (!dealId) {\n throw new Error('Deal ID not found in webhook payload: ' + JSON.stringify(body));\n}\n\nreturn [{ json: { dealId: String(dealId).trim() } }];"
},
"typeVersion": 2
},
{
"id": "cbd4900a-4d7a-42a4-93cb-7e18defcc36d",
"name": "Get Deal from Bitrix24",
"type": "n8n-nodes-base.httpRequest",
"position": [
-610,
20
],
"parameters": {
"url": "={{ $('Settings').first().json.bitrixWebhookBase }}/crm.deal.get.json?ID={{ $json.dealId }}",
"options": {}
},
"typeVersion": 4.4
},
{
"id": "6dced8a9-5455-45be-96b4-1facb1c8f6f7",
"name": "Check Eligibility",
"type": "n8n-nodes-base.code",
"position": [
-210,
20
],
"parameters": {
"jsCode": "// Decides whether this deal should be uploaded to Google Ads.\nconst S = $('Settings').first().json;\nconst deal = $input.first().json.result || {};\n\nconst dealId = String(deal.ID || '').trim();\nif (!dealId) {\n return [{ json: { shouldSend: false, skipReason: 'missing_deal_id' } }];\n}\n\n// Values read from your own Bitrix24 custom fields (configured in Settings).\nconst gclid = String(deal[S.gclidFieldId] || '').trim();\nconst qualifiedRaw = deal[S.qualifiedFieldId];\nconst isQualified = Array.isArray(qualifiedRaw)\n ? qualifiedRaw.map(String).includes(String(S.qualifiedValue))\n : String(qualifiedRaw) === String(S.qualifiedValue);\nconst alreadySent = String(deal[S.sentFlagFieldId] || '') === '1';\n\n// Bitrix24 often fires several identical events for one change. Keep a short\n// in-memory record of recently seen deals so we do not upload duplicates.\nconst staticData = $getWorkflowStaticData('global');\nconst now = Date.now();\nconst DEDUPE_TTL_MS = 2 * 60 * 1000;\nconst key = 'ga_upload_guard_' + dealId;\n\nfor (const [k, v] of Object.entries(staticData)) {\n if (k.startsWith('ga_upload_guard_') && (typeof v !== 'number' || now - v > DEDUPE_TTL_MS)) {\n delete staticData[k];\n }\n}\nconst dedupeHit = typeof staticData[key] === 'number' && now - staticData[key] < DEDUPE_TTL_MS;\n\n// Allow a manual re-send by posting { \"manualRetry\": true } to the webhook.\nconst webhookBody = $('Bitrix24 Deal Webhook').first().json.body || {};\nconst manualRetry = webhookBody.manualRetry === true || String(webhookBody.manualRetry || '').toLowerCase() === 'true';\n\nlet skipReason = null;\nif (!gclid) skipReason = 'missing_gclid';\nelse if (!isQualified) skipReason = 'not_qualified';\nelse if (!manualRetry && alreadySent) skipReason = 'already_sent';\nelse if (!manualRetry && dedupeHit) skipReason = 'duplicate_event';\n\nif (!skipReason) staticData[key] = now;\n\n// Google Ads expects \"YYYY-MM-DD HH:MM:SS+|-HH:MM\".\nconst raw = deal.DATE_MODIFY || deal.DATE_CREATE || new Date().toISOString();\nconst d = new Date(raw);\nconst p = (n) => String(n).padStart(2, '0');\nconst conversionDateTime =\n d.getUTCFullYear() + '-' + p(d.getUTCMonth() + 1) + '-' + p(d.getUTCDate()) + ' ' +\n p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ':' + p(d.getUTCSeconds()) + '+00:00';\n\nconst value = Number.parseFloat(deal.OPPORTUNITY || 0);\n\nreturn [{\n json: {\n shouldSend: !skipReason,\n skipReason,\n dealId,\n gclid,\n contactId: String(deal.CONTACT_ID || '').trim(),\n conversionValue: Number.isFinite(value) ? value : 0,\n currency: deal.CURRENCY_ID || S.fallbackCurrency,\n conversionDateTime,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "c7122ea2-4909-403f-a6df-11a259c73b55",
"name": "Eligible for Upload?",
"type": "n8n-nodes-base.filter",
"position": [
30,
20
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "32c53f45-5d39-4f03-bd9e-5e964257b5d6",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.shouldSend }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "7193c7df-b95f-46d5-816f-bc578c97156f",
"name": "Get Contact from Bitrix24",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
430,
20
],
"parameters": {
"url": "={{ $('Settings').first().json.bitrixWebhookBase }}/crm.contact.get.json?ID={{ $json.contactId }}",
"options": {}
},
"typeVersion": 4.4,
"alwaysOutputData": true
},
{
"id": "a3f28a34-8425-47c0-8e8b-0df57ca08417",
"name": "Normalize Customer Data",
"type": "n8n-nodes-base.code",
"position": [
670,
20
],
"parameters": {
"jsCode": "// Normalises customer data exactly as Google Ads requires before hashing:\n// lower-cased trimmed email (with gmail dot/plus rules), E.164 phone,\n// lower-cased names, ISO-3166-1 alpha-2 country.\nconst source = $('Check Eligibility').first().json;\nconst contact = ($input.first().json && $input.first().json.result) || {};\n\nfunction firstValue(list) {\n if (!Array.isArray(list)) return '';\n for (const item of list) {\n const v = String((item && item.VALUE) || '').trim();\n if (v) return v;\n }\n return '';\n}\n\nfunction normalizeEmail(input) {\n const value = String(input || '').trim().toLowerCase();\n const parts = value.split('@');\n if (parts.length !== 2 || !parts[0] || !parts[1]) return '';\n let [local, domain] = parts;\n if (domain === 'gmail.com' || domain === 'googlemail.com') {\n local = local.split('+')[0].replace(/\\./g, '');\n }\n return local + '@' + domain;\n}\n\nfunction normalizePhone(input) {\n let value = String(input || '').replace(/[^\\d+]/g, '');\n if (value.startsWith('00')) value = '+' + value.slice(2);\n value = '+' + value.replace(/\\D/g, '');\n return value.length < 9 ? '' : value;\n}\n\nfunction toIso2(input) {\n const value = String(input || '').trim();\n if (/^[A-Za-z]{2}$/.test(value)) return value.toUpperCase();\n // Extend this map with the country names your CRM actually stores.\n const map = {\n 'united states': 'US', usa: 'US', 'united kingdom': 'GB', uk: 'GB',\n canada: 'CA', germany: 'DE', france: 'FR', spain: 'ES', italy: 'IT',\n netherlands: 'NL', poland: 'PL', india: 'IN', australia: 'AU', brazil: 'BR',\n };\n return map[value.toLowerCase()] || '';\n}\n\nreturn [{\n json: {\n dealId: source.dealId,\n gclid: source.gclid,\n conversionValue: source.conversionValue,\n currency: source.currency,\n conversionDateTime: source.conversionDateTime,\n normalizedEmail: normalizeEmail(firstValue(contact.EMAIL)),\n normalizedPhone: normalizePhone(firstValue(contact.PHONE)),\n normalizedFirstName: String(contact.NAME || '').trim().toLowerCase(),\n normalizedLastName: String(contact.LAST_NAME || '').trim().toLowerCase(),\n countryCode: toIso2(contact.ADDRESS_COUNTRY),\n postalCode: String(contact.ADDRESS_POSTAL_CODE || '').trim().toUpperCase().replace(/\\s+/g, ' '),\n },\n}];"
},
"typeVersion": 2
},
{
"id": "d4456978-104b-4f49-a556-6f6beac1f0e5",
"name": "Hash Email",
"type": "n8n-nodes-base.crypto",
"position": [
910,
20
],
"parameters": {
"type": "SHA256",
"value": "={{ $json.normalizedEmail || 'unset' }}",
"action": "hash",
"encoding": "hex",
"dataPropertyName": "hashedEmail"
},
"typeVersion": 2
},
{
"id": "a533c667-85e8-4ef6-a9c6-a4bc5659fb33",
"name": "Hash Phone",
"type": "n8n-nodes-base.crypto",
"position": [
1150,
20
],
"parameters": {
"type": "SHA256",
"value": "={{ $json.normalizedPhone || 'unset' }}",
"action": "hash",
"encoding": "hex",
"dataPropertyName": "hashedPhone"
},
"typeVersion": 2
},
{
"id": "d1451851-ae97-4375-ad0b-f76b886a36ad",
"name": "Hash First Name",
"type": "n8n-nodes-base.crypto",
"position": [
1390,
20
],
"parameters": {
"type": "SHA256",
"value": "={{ $json.normalizedFirstName || 'unset' }}",
"action": "hash",
"encoding": "hex",
"dataPropertyName": "hashedFirstName"
},
"typeVersion": 2
},
{
"id": "b6a79a5d-9590-44ce-98e1-58484bb94fb8",
"name": "Hash Last Name",
"type": "n8n-nodes-base.crypto",
"position": [
1630,
20
],
"parameters": {
"type": "SHA256",
"value": "={{ $json.normalizedLastName || 'unset' }}",
"action": "hash",
"encoding": "hex",
"dataPropertyName": "hashedLastName"
},
"typeVersion": 2
},
{
"id": "3c0a1817-5f1a-4d70-a8c0-e0819d543801",
"name": "Build Conversion Payload",
"type": "n8n-nodes-base.code",
"position": [
2030,
20
],
"parameters": {
"jsCode": "// Assembles the ClickConversion payload. Hashed identifiers are only attached\n// when the underlying value actually existed, so we never send a hash of \"\".\nconst S = $('Settings').first().json;\nconst j = $input.first().json;\n\nconst conversion = {\n gclid: j.gclid,\n conversionAction: 'customers/' + S.googleAdsCustomerId + '/conversionActions/' + S.conversionActionId,\n conversionDateTime: j.conversionDateTime,\n orderId: j.dealId,\n currencyCode: j.currency || S.fallbackCurrency,\n conversionValue: j.conversionValue,\n};\n\nconst userIdentifiers = [];\nif (j.normalizedEmail) {\n userIdentifiers.push({ userIdentifierSource: 'FIRST_PARTY', hashedEmail: j.hashedEmail });\n}\nif (j.normalizedPhone) {\n userIdentifiers.push({ userIdentifierSource: 'FIRST_PARTY', hashedPhoneNumber: j.hashedPhone });\n}\nif (j.normalizedFirstName && j.normalizedLastName && j.countryCode && j.postalCode) {\n userIdentifiers.push({\n userIdentifierSource: 'FIRST_PARTY',\n addressInfo: {\n hashedFirstName: j.hashedFirstName,\n hashedLastName: j.hashedLastName,\n countryCode: j.countryCode,\n postalCode: j.postalCode,\n },\n });\n}\n// Google Ads accepts at most 5 user identifiers per conversion.\nif (userIdentifiers.length) conversion.userIdentifiers = userIdentifiers.slice(0, 5);\n\nreturn [{\n json: {\n dealId: j.dealId,\n gclid: j.gclid,\n requestBody: { conversions: [conversion], partialFailure: true },\n },\n}];"
},
"typeVersion": 2
},
{
"id": "70633ad2-faea-4daf-b32a-a607d7c65f66",
"name": "Upload Click Conversion to Google Ads",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"position": [
2270,
20
],
"parameters": {
"url": "=https://googleads.googleapis.com/{{ $('Settings').first().json.googleAdsApiVersion }}/customers/{{ $('Settings').first().json.googleAdsCustomerId }}:uploadClickConversions",
"body": "={{ JSON.stringify($json.requestBody) }}",
"method": "POST",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "predefinedCredentialType",
"rawContentType": "application/json",
"headerParameters": {
"parameters": [
{
"name": "developer-token",
"value": "={{ $('Settings').first().json.googleAdsDeveloperToken }}"
},
{
"name": "login-customer-id",
"value": "={{ $('Settings').first().json.googleAdsLoginCustomerId }}"
}
]
},
"nodeCredentialType": "googleAdsOAuth2Api"
},
"typeVersion": 4.4
},
{
"id": "9c75cf1f-8b7c-4bc7-b905-a857bce87935",
"name": "Validate Google Ads Response",
"type": "n8n-nodes-base.code",
"position": [
2510,
20
],
"parameters": {
"jsCode": "// Google Ads answers 200 even when a conversion was rejected, so inspect\n// partialFailureError instead of trusting the status code.\nconst payload = $input.first().json || {};\nconst body = payload.body || payload;\n\nconst partialFailure = body.partialFailureError;\nconst pfStr = partialFailure && Object.keys(partialFailure).length ? JSON.stringify(partialFailure) : '';\nconst results = Array.isArray(body.results) ? body.results.filter((r) => r && Object.keys(r).length) : [];\n\n// These outcomes mean \"already recorded\" - treat them as success so the deal\n// gets flagged and we stop retrying forever.\nconst isTerminal = /ALREADY_EXISTS|DUPLICATE|EXPIRED_EVENT/i.test(pfStr);\n\nif (pfStr && !isTerminal) {\n throw new Error('Google Ads partial failure: ' + pfStr);\n}\nif (!results.length && !pfStr) {\n throw new Error('Google Ads returned no conversion results: ' + JSON.stringify(body));\n}\n\nconst source = $('Build Conversion Payload').first().json || {};\n\nreturn [{\n json: {\n uploaded: true,\n dealId: source.dealId || '',\n gclid: source.gclid || '',\n terminalPartialFailure: isTerminal ? pfStr.slice(0, 300) : '',\n uploadedAt: new Date().toISOString(),\n },\n}];"
},
"typeVersion": 2
},
{
"id": "83736a36-5d71-4f3c-8398-95f5f884a34f",
"name": "Log Success in Bitrix24",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 3,
"position": [
2750,
20
],
"parameters": {
"url": "={{ $('Settings').first().json.bitrixWebhookBase }}/crm.timeline.comment.add",
"method": "POST",
"options": {},
"jsonBody": "={{ { \"fields\": { \"ENTITY_ID\": $json.dealId, \"ENTITY_TYPE\": \"deal\", \"COMMENT\": \"Conversion uploaded to Google Ads. GCLID: \" + $json.gclid } } }}",
"sendBody": true,
"specifyBody": "json"
},
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 4000
},
{
"id": "3df9c5e6-beb6-42c0-a35d-ebc6fc692d5e",
"name": "Mark Deal as Sent",
"type": "n8n-nodes-base.httpRequest",
"maxTries": 3,
"position": [
2990,
20
],
"parameters": {
"url": "={{ $('Settings').first().json.bitrixWebhookBase }}/crm.deal.update.json?ID={{ $('Validate Google Ads Response').first().json.dealId }}&FIELDS[{{ $('Settings').first().json.sentFlagFieldId }}]=1",
"options": {}
},
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 4000
},
{
"id": "f12d4ae6-29e7-419c-b78f-bcaf64a0cacb",
"name": "Log Error in Bitrix24",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
2030,
220
],
"parameters": {
"url": "={{ $('Settings').first().json.bitrixWebhookBase }}/crm.timeline.comment.add",
"method": "POST",
"options": {},
"jsonBody": "={{ { \"fields\": { \"ENTITY_ID\": $('Build Conversion Payload').first().json.dealId, \"ENTITY_TYPE\": \"deal\", \"COMMENT\": \"Google Ads upload failed. GCLID: \" + $('Build Conversion Payload').first().json.gclid + \"\\nResponse: \" + JSON.stringify($json.error || $json.body || $json).substring(0, 1000) } } }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.4
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Settings": {
"main": [
[
{
"node": "Extract Deal ID",
"type": "main",
"index": 0
}
]
]
},
"Hash Email": {
"main": [
[
{
"node": "Hash Phone",
"type": "main",
"index": 0
}
]
]
},
"Hash Phone": {
"main": [
[
{
"node": "Hash First Name",
"type": "main",
"index": 0
}
]
]
},
"Hash Last Name": {
"main": [
[
{
"node": "Build Conversion Payload",
"type": "main",
"index": 0
}
]
]
},
"Extract Deal ID": {
"main": [
[
{
"node": "Get Deal from Bitrix24",
"type": "main",
"index": 0
}
]
]
},
"Hash First Name": {
"main": [
[
{
"node": "Hash Last Name",
"type": "main",
"index": 0
}
]
]
},
"Check Eligibility": {
"main": [
[
{
"node": "Eligible for Upload?",
"type": "main",
"index": 0
}
]
]
},
"Eligible for Upload?": {
"main": [
[
{
"node": "Get Contact from Bitrix24",
"type": "main",
"index": 0
}
]
]
},
"Bitrix24 Deal Webhook": {
"main": [
[
{
"node": "Settings",
"type": "main",
"index": 0
}
]
]
},
"Get Deal from Bitrix24": {
"main": [
[
{
"node": "Check Eligibility",
"type": "main",
"index": 0
}
]
]
},
"Log Success in Bitrix24": {
"main": [
[
{
"node": "Mark Deal as Sent",
"type": "main",
"index": 0
}
]
]
},
"Normalize Customer Data": {
"main": [
[
{
"node": "Hash Email",
"type": "main",
"index": 0
}
]
]
},
"Build Conversion Payload": {
"main": [
[
{
"node": "Upload Click Conversion to Google Ads",
"type": "main",
"index": 0
}
]
]
},
"Get Contact from Bitrix24": {
"main": [
[
{
"node": "Normalize Customer Data",
"type": "main",
"index": 0
}
]
]
},
"Validate Google Ads Response": {
"main": [
[
{
"node": "Log Success in Bitrix24",
"type": "main",
"index": 0
}
]
]
},
"Upload Click Conversion to Google Ads": {
"main": [
[
{
"node": "Validate Google Ads Response",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Error in Bitrix24",
"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 receives Bitrix24 deal updates via a webhook, fetches the deal and contact details, and uploads an offline Purchase conversion to the Google Ads Click Conversion API using the deal’s GCLID and value, then writes success or error notes back to the deal and flags it…
Source: https://n8n.io/workflows/17104/ — 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 workflow triggers on Bitrix24 deal updates, checks whether a deal is qualified and has a GCLID, then uploads an offline click conversion to the Google Ads API with hashed first-party customer ide
Automatically qualify, score, and route inbound B2B leads using GPT-4o-mini — no manual review needed.
PostStack: Leady z DM → Google Sheets. Uses httpRequest, googleSheets. Webhook trigger; 18 nodes.
This workflow receives Zivvy CRM lead creation webhooks, verifies the HMAC signature, fetches lead details from the Zivvy API, and posts a formatted notification to Slack via an incoming webhook. Rece
This workflow uses KlickTipp community nodes, available for self-hosted n8n instances only.