This workflow corresponds to n8n.io template #13621 — 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": "G5K8zclSBhyHKgrPEox1M",
"name": "Transfer scanned vCard QR codes data to KlickTipp",
"tags": [
{
"id": "15wrq9sti6wyqr6J",
"name": "TEMPLATE",
"createdAt": "2025-01-08T16:34:30.163Z",
"updatedAt": "2025-01-08T16:34:30.163Z"
}
],
"nodes": [
{
"id": "fe60251e-5a6a-44db-97ea-f7f562cadfc3",
"name": "Documentation",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1184,
-352
],
"parameters": {
"width": 695,
"height": 1164,
"content": "Community Node Disclaimer: This workflow uses KlickTipp community nodes.\n\n## Introduction\nThis workflow captures **vCard contact data from a scanned QR code** using **AllCodeRelay** and automatically **creates a contact in KlickTipp**. It is built for event lead capture and fast contact transfer: scan a vCard QR code, send the scan result to a webhook, parse the vCard into clean contact fields, and submit the contact to KlickTipp with **Double Opt-In (DOI)**.\n\nThis template is ideal for marketers, sales teams, and event staff who want a frictionless way to digitize business cards and subscribe contacts into KlickTipp in real time.\n\n## How it works\n1. **Trigger: Scan Webhook from AllCodeRelay**\n - AllCodeRelay sends the scan payload to an n8n webhook endpoint.\n2. **Filter: vCard Only**\n - Only payloads containing a valid vCard are processed (e.g., `BEGIN:VCARD ... END:VCARD`).\n3. **Parse: vCard \u2192 Contact Fields**\n - Extracts and normalizes these fields for mapping:\n - `firstName`, `lastName`, `email`, `mobile`, `fax`, `phone`\n - `company`, `position`\n - `street`, `city`, `state`, `zip`, `country`\n - `website`\n4. **KlickTipp: Add Contact with DOI**\n - Creates or updates the subscriber and triggers DOI.\n\n## Setup Instructions\n1. **AllCodeRelay configuration**\n - Install AllCodeRelay on your phone.\n - Create a webhook destination pointing to the n8n webhook URL from Step 2.\n - Scan a **static vCard QR code**.\n\n2. **n8n configuration**\n - Open the first node (Webhook) and copy the **Test URL** (or Production URL after publishing).\n - In AllCodeRelay, paste the URL as the webhook target.\n - Run a test scan and verify that the workflow receives `BEGIN:VCARD` payloads.\n\n3. **KlickTipp configuration**\n - Create a segmentation **tag** (e.g., `vCard QR code scan`)\n - Authenticate your KlickTipp connection with **username/password** credentials (API access required).\n - Map the parsed fields to the **\"KlickTipp: Add Contact with DOI\"** node.\n - Select DOI opt-in process and tag **\"vCard QR code scan\"**.\n\n## Notes\n- This template supports **vCard QR codes** where the QR content is the vCard text itself (static/offline vCard).\n- If your QR code resolves to a short URL (dynamic QR), the scan will only return the URL and will not contain embedded vCard data.\n- Extend parsing to support additional vCard fields if needed (e.g., multiple emails, additional addresses).\n"
},
"typeVersion": 1
},
{
"id": "ff10f325-29df-41b6-9344-f8dced91f5c6",
"name": "3. Parse data",
"type": "n8n-nodes-base.stickyNote",
"position": [
-32,
-352
],
"parameters": {
"color": 7,
"width": 216,
"height": 364,
"content": "## 3. Parse data"
},
"typeVersion": 1
},
{
"id": "6a51df39-d4a8-4b78-8947-96b1c496af10",
"name": "4. Save data",
"type": "n8n-nodes-base.stickyNote",
"position": [
192,
-352
],
"parameters": {
"color": 7,
"width": 248,
"height": 364,
"content": "## 4. Save data"
},
"typeVersion": 1
},
{
"id": "b56bab82-0950-4744-b4bb-f5d0d7abfa0f",
"name": "2. Filter data",
"type": "n8n-nodes-base.stickyNote",
"position": [
-240,
-352
],
"parameters": {
"color": 7,
"width": 200,
"height": 364,
"content": "## 2. Filter data"
},
"typeVersion": 1
},
{
"id": "6fbefa0f-9d6e-4a8f-803e-6e81e7c8318a",
"name": "1. Get data",
"type": "n8n-nodes-base.stickyNote",
"position": [
-480,
-352
],
"parameters": {
"color": 7,
"width": 232,
"height": 364,
"content": "## 1. Get data"
},
"typeVersion": 1
},
{
"id": "805e3da5-52e2-48a6-9458-e7a6d2a674e7",
"name": "Filter: vCard Only",
"type": "n8n-nodes-base.filter",
"position": [
-192,
-208
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1bebea66-1800-4d98-b610-9caaf215b579",
"operator": {
"type": "string",
"operation": "startsWith"
},
"leftValue": "={{ $json.body.code }}",
"rightValue": "BEGIN:VCARD"
},
{
"id": "58da7eab-7eb8-4f0c-84e1-cded0e704bf6",
"operator": {
"type": "string",
"operation": "endsWith"
},
"leftValue": "={{ $json.body.code }}",
"rightValue": "END:VCARD"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "ec1aa8d3-2523-469c-b9e1-74cc182d13e7",
"name": "Parse: vCard \u2192 Contact Fields",
"type": "n8n-nodes-base.code",
"position": [
32,
-208
],
"parameters": {
"jsCode": "const items = $input.all();\n\nconst normalize = (s) =>\n String(s ?? \"\")\n .replace(/\\r\\n/g, \"\\n\")\n .replace(/\\r/g, \"\\n\")\n .trim();\n\n// vCard folded lines: newline + space/tab continues previous line\nconst unfold = (s) => s.replace(/\\n[ \\t]/g, \"\");\n\nconst pickFirst = (arr) => (arr && arr.length ? arr[0].value : \"\");\n\nconst hasParam = (entry, param) =>\n (entry.params || []).some((p) => String(p).toUpperCase() === param);\n\nconst pickTel = (telEntries, wantedParam) => {\n if (!telEntries?.length) return \"\";\n const found = telEntries.find((e) => hasParam(e, wantedParam));\n return found?.value || \"\";\n};\n\nconst pickEmail = (emailEntries) => {\n if (!emailEntries?.length) return \"\";\n // prefer WORK if present, else first\n const work = emailEntries.find((e) => hasParam(e, \"WORK\"));\n return (work || emailEntries[0]).value || \"\";\n};\n\nreturn items.map((item) => {\n const body = item.json.body;\n\n // Most common shape (your screenshot): body is object and vCard is in body.code\n const vcardRaw =\n (typeof body === \"object\" && body?.code) ? body.code :\n (typeof body === \"string\") ? body :\n item.json.code || \"\";\n\n const vcard = unfold(normalize(vcardRaw));\n if (!vcard.startsWith(\"BEGIN:VCARD\")) throw new Error(\"No vCard found in payload\");\n\n // Parse into: { KEY: [ { params:[], value:\"...\" }, ... ] }\n const parsed = {};\n for (const line of vcard.split(\"\\n\")) {\n if (!line || line.startsWith(\"BEGIN:\") || line.startsWith(\"END:\") || line.startsWith(\"VERSION:\")) continue;\n\n const idx = line.indexOf(\":\");\n if (idx === -1) continue;\n\n const left = line.slice(0, idx);\n const value = line.slice(idx + 1).trim();\n\n const parts = left.split(\";\");\n const key = parts[0].toUpperCase();\n const params = parts.slice(1).map((p) => String(p).toUpperCase());\n\n if (!parsed[key]) parsed[key] = [];\n parsed[key].push({ params, value });\n }\n\n // Name\n let firstName = \"\";\n let lastName = \"\";\n\n const nVal = pickFirst(parsed.N); // \"Last;First;...\"\n if (nVal) {\n const [ln = \"\", fn = \"\"] = nVal.split(\";\");\n firstName = fn;\n lastName = ln;\n } else {\n // fallback: split FN\n const fn = pickFirst(parsed.FN).trim();\n if (fn) {\n const seg = fn.split(/\\s+/);\n if (seg.length >= 2) {\n lastName = seg.pop();\n firstName = seg.join(\" \");\n } else {\n firstName = fn;\n }\n }\n }\n\n // Address (ADR: POBOX;EXT;STREET;CITY;REGION;POSTAL;COUNTRY)\n const adrVal = pickFirst(parsed.ADR);\n let street = \"\", city = \"\", state = \"\", zip = \"\", country = \"\";\n if (adrVal) {\n const adrParts = adrVal.split(\";\");\n street = adrParts[2] || \"\";\n city = adrParts[3] || \"\";\n state = adrParts[4] || \"\";\n zip = adrParts[5] || \"\";\n country= adrParts[6] || \"\";\n }\n\n // Phones\n const tel = parsed.TEL || [];\n const mobile = pickTel(tel, \"CELL\") || pickTel(tel, \"MOBILE\");\n const fax = pickTel(tel, \"FAX\");\n const phone = pickTel(tel, \"WORK\") || pickTel(tel, \"VOICE\") || (tel[0]?.value || \"\");\n\n // Email / Company / Position / Website\n const email = pickEmail(parsed.EMAIL);\n const company = pickFirst(parsed.ORG);\n const position = pickFirst(parsed.TITLE);\n const website = pickFirst(parsed.URL);\n\n return {\n json: {\n firstName,\n lastName,\n email,\n mobile,\n fax,\n phone,\n company,\n position,\n street,\n city,\n state,\n country,\n zip,\n website,\n },\n };\n});"
},
"typeVersion": 2
},
{
"id": "15e80a2a-dd05-4d7b-a374-aeef1bcfa6a4",
"name": "AllCodeRelay: Scan Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-416,
-208
],
"parameters": {
"path": "qr-code-scanner",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "f2ed5fa0-276d-473c-9de1-9feba4679e9d",
"name": "KlickTipp: Add Contact with DOI",
"type": "n8n-nodes-klicktipp.klicktipp",
"position": [
256,
-208
],
"parameters": {
"email": "={{ $json.email }}",
"tagId": "14145915",
"fields": {
"dataFields": [
{
"fieldId": "fieldFirstName",
"fieldValue": "={{ $json.firstName }}"
},
{
"fieldId": "fieldLastName",
"fieldValue": "={{ $json.lastName }}"
},
{
"fieldId": "fieldCompanyName",
"fieldValue": "={{ $json.company }}"
},
{
"fieldId": "fieldPhone",
"fieldValue": "={{ $json.phone }}"
},
{
"fieldId": "fieldMobilePhone",
"fieldValue": "={{ $json.mobile }}"
},
{
"fieldId": "fieldFax",
"fieldValue": "={{ $json.fax }}"
},
{
"fieldId": "fieldWebsite",
"fieldValue": "={{ $json.website }}"
},
{
"fieldId": "fieldStreet1",
"fieldValue": "={{ $json.street }}"
},
{
"fieldId": "fieldCity",
"fieldValue": "={{ $json.city }}"
},
{
"fieldId": "fieldState",
"fieldValue": "={{ $json.state }}"
},
{
"fieldId": "fieldCountry",
"fieldValue": "={{ $json.country }}"
},
{
"fieldId": "fieldZip",
"fieldValue": "={{ $json.zip }}"
}
]
},
"listId": "358895",
"resource": "subscriber",
"operation": "subscribe",
"smsNumber": "={{ $json.phone }}"
},
"credentials": {},
"typeVersion": 3
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "a5c02d70-688a-49bd-baf1-462007049441",
"connections": {
"Filter: vCard Only": {
"main": [
[
{
"node": "Parse: vCard \u2192 Contact Fields",
"type": "main",
"index": 0
}
]
]
},
"AllCodeRelay: Scan Webhook": {
"main": [
[
{
"node": "Filter: vCard Only",
"type": "main",
"index": 0
}
]
]
},
"Parse: vCard \u2192 Contact Fields": {
"main": [
[
{
"node": "KlickTipp: Add Contact with DOI",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Community Node Disclaimer: This workflow uses KlickTipp community nodes.
Source: https://n8n.io/workflows/13621/ — 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.
A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without
Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.
This n8n template demonstrates how a simple Multi-Layer Perceptron (MLP) neural network can predict housing prices. The prediction is based on four key features, processed through a three-layer model.
github code Try yourself
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.