{
  "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
          }
        ]
      ]
    }
  }
}