{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "0659ddd3-cbad-4f95-a993-f2362e5fdd97",
      "name": "Crypto",
      "type": "n8n-nodes-base.crypto",
      "notes": "This node prepares the response to the Zoom endpoint in order to answer to the validation.",
      "position": [
        -160,
        -112
      ],
      "parameters": {
        "type": "SHA256",
        "value": "={{ $json.body.payload.plainToken }}",
        "action": "hmac",
        "secret": "YOUR_CREDENTIAL_HERE"
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "f66d1b66-7059-4261-94df-f5fa65bea7a1",
      "name": "Build Validation Body",
      "type": "n8n-nodes-base.set",
      "notes": "This node builds the body for the response to the Zoom endpoint in order to answer for validation.",
      "position": [
        64,
        -112
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "plainToken",
              "value": "={{ $json.body.payload.plainToken }}"
            },
            {
              "name": "encryptedToken",
              "value": "={{ $json.data }}"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "e6cdca55-ccaf-4786-8dc5-530ef2cc0394",
      "name": "Respond to Webhook (Validation)",
      "type": "n8n-nodes-base.respondToWebhook",
      "notes": "This node sends the response back to the Zoom endpoint.",
      "position": [
        272,
        -112
      ],
      "parameters": {
        "options": {}
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "106611cd-1631-4fd8-871d-28fdda7ee5b2",
      "name": "Build Normal Event Body",
      "type": "n8n-nodes-base.set",
      "notes": "This node builds a body for a response to the endpoint in case of a regular event.",
      "position": [
        -160,
        80
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "status",
              "value": "ok"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "dbec7433-2faf-47d0-8f06-6d7c4c20971b",
      "name": "Respond to Webhook (Events)",
      "type": "n8n-nodes-base.respondToWebhook",
      "notes": "This node answers to the endpoint in case of regular Zoom events - such as ending meetings in this case.",
      "position": [
        64,
        80
      ],
      "parameters": {
        "options": {}
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "dd1d32e6-ab68-4aaf-b1f4-6df83223724e",
      "name": "URL Validation check",
      "type": "n8n-nodes-base.if",
      "notes": "This node checks whether the received data was a ending meeting or a URL validation call by Zoom.",
      "position": [
        -384,
        -16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8516a75f-6aa9-4b72-828a-f5627d066ecc",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.body.event }}",
              "rightValue": "endpoint.url_validation"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.2
    },
    {
      "id": "78ecf8f8-9fb4-4b14-b079-6038e0693c79",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -656,
        -304
      ],
      "parameters": {
        "color": 4,
        "width": 1104,
        "height": 752,
        "content": "## Meeting data reception\n"
      },
      "typeVersion": 1
    },
    {
      "id": "ee6d0810-3968-481b-b214-dd81e7afeb69",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        -304
      ],
      "parameters": {
        "color": 2,
        "width": 448,
        "height": 752,
        "content": "## Pulling webinar data - participants and absentees\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b115c42e-89c4-48ab-aad8-2557978053bd",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        -304
      ],
      "parameters": {
        "color": 3,
        "width": 1312,
        "height": 752,
        "content": "## Segment all participants and absentees from the past webinar\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "6de2ce31-dca8-43aa-ab89-4aeebc327822",
      "name": "Tag participant with full attendance1",
      "type": "CUSTOM.klicktipp",
      "notes": "This node tags the contacts of your first webinar branch  for the full attendance if they have attended more than 90% of the webinar.",
      "position": [
        2016,
        224
      ],
      "parameters": {
        "email": "={{ $json.email }}",
        "tagId": [
          "13445224"
        ],
        "resource": "contact-tagging"
      },
      "credentials": {
        "klickTippApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "27d21cf7-6378-466d-b196-bf11806e2ed8",
      "name": "Tag participant for full attendance",
      "type": "CUSTOM.klicktipp",
      "notes": "This node tags the contacts of your first webinar branch  for the full attendance if they have attended more than 90% of the webinar.",
      "position": [
        2000,
        -48
      ],
      "parameters": {
        "email": "={{ $json.email }}",
        "tagId": [
          "13440472"
        ],
        "resource": "contact-tagging"
      },
      "credentials": {
        "klickTippApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "5296bb95-1c28-4a95-b776-11997cf344be",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 1104,
        "height": 1920,
        "content": "Community Node Disclaimer: This workflow uses KlickTipp community nodes.\n\n\n### Introduction\nThis workflow automates the end-to-end integration between Zoom and KlickTipp using n8n. It listens to Zoom meeting events (specifically, when a meeting ends), fetches attendance data, subscribes the participants and tags them in KlickTipp based on how long they attended. This enables precise segmentation for follow-ups and campaign automation based on actual user engagement.\n\n### Benefits\n- **Fully automated tagging**: No manual effort\u2014participants are tagged in KlickTipp based on attendance percentage.\n- **Precise segmentation**: Supports differentiated tags for full, partial, or no attendance per meeting type.\n- **Dual-meeting support**: Handles multiple meeting types (e.g. Beginner vs. Expert) in one automated flow.\n\n### Key Features\n- **Zoom Webhook Listener**:\n  - Reacts to `meeting.ended` events.\n  - Validates Zoom URL on webhook registration using HMAC.\n- **Zoom API Integration**:\n  - Pulls past meeting details and participant list using meeting UUID.\n  - Uses `duration` fields to calculate attendance.\n- **Conditional Logic**:\n  - Uses Switch nodes to differentiate meetings by topic.\n  - Filters out internal users.\n  - Applies IF logic to check attendance thresholds (e.g. \u2265\u202f90%, \u2265\u202f60%).\n- **KlickTipp Subscription & Tagging**:\n  - Subscribes participant to KlickTipp based on meeting data\n  - Applies different tags for each attendance segment:\n    - Full attendance (\u2265\u202f90%)\n    - General attendance\n    - No attendance\n  - Supports tagging per meeting name (ex.: meeting E-Mail Zustellung f\u00fcr Anf\u00e4nger vs. meeting E-Mail Zustellung f\u00fcr Experten).\n- **Scalable Structure**:\n  - Easily extendable to more meetings by duplicating the Switch and tagging logic.\n\n### Setup Instructions\n**Zoom Preparation**\n   - Ensure Zoom API access is enabled.\n   - Set up recurring meetings or meetings.\n   - Enable webhook event `meeting.ended` for your Zoom webhook and the scopes `meeting:read:meeting`, `meeting:read:list_past_participants`.\n\n**KlickTipp Preparation**\n   - Create these custom fields (if not already present):\n  - `Zoom | meeting selection` (Text)\n- `Zoom | meeting start` (Date & Time)\n- `Zoom | Join URL` (URL)\n- `Zoom | Registration ID` (Text)\n- `Zoom | Duration meeting` (Text)\n\n  - Manually add the following tags:\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Anf\u00e4nger`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Anf\u00e4nger attended`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Anf\u00e4nger attended fully`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Anf\u00e4nger not attended`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Experten`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Experten attended`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Experten attended fully`\n     - `Zoom meeting E-Mail Zustellung f\u00fcr Experten not attended`\n\n\n**n8n Configuration**\n   - Configure the webhook node for the Zoom endpoint and connect it to your webhook.\n   - Set up API credentials for Zoom and KlickTipp.\n  \n\n### Testing and Deployment\n1. End a Zoom meeting connected to the flow.\n2. Verify:\n   - The webhook triggers.\n   - Participants are fetched from Zoom.\n   - Tags are applied correctly in KlickTipp.\n3. Review KlickTipp contact records for tag and custom field updates.\n\n> \ud83d\udca1 *Pro Tip*: Use test emails and simulate different durations to validate tagging logic.\n\n### Campaign Expansion Ideas\n- Add additional meeting topics and tag logic by extending the Switch node.\n- Trigger personalized email sequences in KlickTipp based on attendance status.\n- Combine this workflow with registration tracking for full-funnel automation.\n- Use conditional routing to send follow-ups only to specific attendance segments.\n\n### Customization\n- Update tag names in the KlickTipp nodes to match your campaign naming convention.\n- Adjust attendance thresholds (e.g. 80% instead of 90%) in IF nodes.\n- Add fallback conditions for missing data or error handling on Zoom API calls.\n- Use more granular time ranges for segmentation of attendance (e.g. 0\u201330%, 30\u201360%, etc.).\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4ac87f98-1281-41d5-b607-3a4e2919ac2e",
      "name": "Filter internal users",
      "type": "n8n-nodes-base.filter",
      "notes": "This node filters out internal users from the webinar such as the host.",
      "position": [
        976,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "6c66e073-723a-44a2-83c7-0ece10385a3b",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Split webinar participants').item.json.internal_user }}",
              "rightValue": ""
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.2
    },
    {
      "id": "fc1718e5-321e-4e7e-8cdd-cd6bf34e78e6",
      "name": "Split webinar participants",
      "type": "n8n-nodes-base.splitOut",
      "notes": "This node iterates through all participants.",
      "position": [
        752,
        80
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "participants"
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "3a7eb266-fc42-4448-a750-287230ccc4cc",
      "name": "Route by webinar name",
      "type": "n8n-nodes-base.switch",
      "notes": "This node filters for the different webinars via the name in order to process them.",
      "position": [
        1184,
        80
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "E-Mail Zustellung f\u00fcr Anf\u00e4nger",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "8de9cdc6-5393-4daa-acbd-7d14fbcd2586",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    },
                    "leftValue": "={{ $('Listen to ending Zoom meetings').item.json.body.payload.object.topic }}",
                    "rightValue": "E-Mail Zustellung f\u00fcr Anf\u00e4nger"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "E-Mail Zustellung f\u00fcr Experten",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "bb59b2cb-b22c-4325-8373-f2a50dcb5ef1",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    },
                    "leftValue": "={{ $('Listen to ending Zoom meetings').item.json.body.payload.object.topic }}",
                    "rightValue": "E-Mail Zustellung f\u00fcr Experten"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "notesInFlow": true,
      "typeVersion": 3.2
    },
    {
      "id": "9e068327-0a37-41a0-95b0-31fdd60ae887",
      "name": "Check full attendance",
      "type": "n8n-nodes-base.if",
      "notes": "This node checks whether the participants have spent more than 90% in the webinar or not.",
      "position": [
        1744,
        -32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "55a73ac0-4754-4ae4-b6ac-cc3b46b19580",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $('Split webinar participants').item.json.duration }}",
              "rightValue": "={{ $('Listen to ending Zoom meetings').item.json.body.payload.object.duration * 60 * 0.9 }}"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.2
    },
    {
      "id": "6da889bc-4e87-4a27-93bb-a3875469ab7b",
      "name": "Check full attendance1",
      "type": "n8n-nodes-base.if",
      "notes": "This node checks whether the participants have spent more than 90% in the webinar or not.",
      "position": [
        1760,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f3673baa-a23b-4f19-91d9-5b1bac26e064",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $('Split webinar participants').item.json.duration }}",
              "rightValue": "={{ $('Listen to ending Zoom meetings').item.json.body.payload.object.duration * 60 * 0.9 }}"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.2
    },
    {
      "id": "3368dabc-71e4-4b5d-a5d7-2cd767806672",
      "name": "Wait 1 second",
      "type": "n8n-nodes-base.wait",
      "notes": "This node waits 1 second to avoid calling the past webinar to early before it is available.",
      "position": [
        272,
        80
      ],
      "parameters": {
        "amount": 1
      },
      "notesInFlow": true,
      "typeVersion": 1.1
    },
    {
      "id": "05b7c7ee-4a94-4f6f-b8a7-daddb0f774c9",
      "name": "Listen to ending Zoom meetings",
      "type": "n8n-nodes-base.webhook",
      "notes": "This node is listening to Zoom events and triggers when a webinar was ended.",
      "position": [
        -608,
        -16
      ],
      "parameters": {
        "path": "0149ebe4-2dd9-420d-af7c-d60439f22451",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "notesInFlow": true,
      "typeVersion": 2.1
    },
    {
      "id": "0c825304-48c3-4d03-9228-9a0f4b806b62",
      "name": "Get past Zoom meeting participants",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "This node pulls data of the webinar participants in order to process them.",
      "position": [
        512,
        80
      ],
      "parameters": {
        "url": "=https://api.zoom.us/v2/past_meetings/{{ $('Listen to ending Zoom meetings').item.json.body.payload.object.uuid }}/participants",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "zoomOAuth2Api"
      },
      "credentials": {
        "zoomOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 4.2
    },
    {
      "id": "fc622fae-a156-41a2-a10f-601f8aca4dc5",
      "name": "Subscribe participant",
      "type": "n8n-nodes-klicktipp.klicktipp",
      "notes": "This node subscribes the participant based on the meeting data as a new contact in KlickTipp or updates it.",
      "position": [
        1536,
        -32
      ],
      "parameters": {
        "email": "={{ $json.user_email }}",
        "tagId": "13327964",
        "fields": {
          "dataFields": [
            {
              "fieldId": "fieldFirstName",
              "fieldValue": "={{ \n  // Take the \"name\" field from the current JSON item\n  $json.name\n    // Remove leading/trailing spaces (just in case)\n    .trim()\n    // Split the string into an array, using a space as the separator\n    .split(\" \")\n    // Select the first element of the array (the first word)\n    [0] \n}}"
            },
            {
              "fieldId": "fieldLastName",
              "fieldValue": "={{ \n  /* take \"name\" or empty string if missing */ ($json.name || '')\n    /* trim leading/trailing whitespace */ .trim()\n    /* split into words by any whitespace */ .split(/\\s+/)\n    /* get last word */ .slice(-1)[0]\n    /* fallback */ || ''\n}}"
            }
          ]
        },
        "listId": "358895",
        "resource": "subscriber",
        "operation": "subscribe"
      },
      "credentials": {
        "klickTippApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 3
    },
    {
      "id": "bd71ca87-dbd7-4525-9ae6-a072069baa67",
      "name": "Subscribe participant1",
      "type": "n8n-nodes-klicktipp.klicktipp",
      "notes": "This node subscribes the participant based on the meeting data as a new contact in KlickTipp or updates it.",
      "position": [
        1536,
        240
      ],
      "parameters": {
        "email": "={{ $json.user_email }}",
        "tagId": "13327965",
        "fields": {
          "dataFields": [
            {
              "fieldId": "fieldFirstName",
              "fieldValue": "={{ \n  // Take the \"name\" field from the current JSON item\n  $json.name\n    // Remove leading/trailing spaces (just in case)\n    .trim()\n    // Split the string into an array, using a space as the separator\n    .split(\" \")\n    // Select the first element of the array (the first word)\n    [0] \n}}"
            },
            {
              "fieldId": "fieldLastName",
              "fieldValue": "={{    /* take \"name\" or empty string if missing */ ($json.name || '')     /* trim leading/trailing whitespace */ .trim()     /* split into words by any whitespace */ .split(/\\s+/)     /* get last word */ .slice(-1)[0]     /* fallback */ || '' }}"
            }
          ]
        },
        "listId": "358895",
        "resource": "subscriber",
        "operation": "subscribe"
      },
      "credentials": {
        "klickTippApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 3
    }
  ],
  "connections": {
    "Crypto": {
      "main": [
        [
          {
            "node": "Build Validation Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 1 second": {
      "main": [
        [
          {
            "node": "Get past Zoom meeting participants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "URL Validation check": {
      "main": [
        [
          {
            "node": "Crypto",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Normal Event Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Validation Body": {
      "main": [
        [
          {
            "node": "Respond to Webhook (Validation)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check full attendance": {
      "main": [
        [
          {
            "node": "Tag participant for full attendance",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Filter internal users": {
      "main": [
        [
          {
            "node": "Route by webinar name",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by webinar name": {
      "main": [
        [
          {
            "node": "Subscribe participant",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Subscribe participant1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Subscribe participant": {
      "main": [
        [
          {
            "node": "Check full attendance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check full attendance1": {
      "main": [
        [
          {
            "node": "Tag participant with full attendance1",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Subscribe participant1": {
      "main": [
        [
          {
            "node": "Check full attendance1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Normal Event Body": {
      "main": [
        [
          {
            "node": "Respond to Webhook (Events)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split webinar participants": {
      "main": [
        [
          {
            "node": "Filter internal users",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond to Webhook (Events)": {
      "main": [
        [
          {
            "node": "Wait 1 second",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Listen to ending Zoom meetings": {
      "main": [
        [
          {
            "node": "URL Validation check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get past Zoom meeting participants": {
      "main": [
        [
          {
            "node": "Split webinar participants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}