{
  "id": "sTHPXMtsLWWknYKf",
  "name": "1. User verification OTP Onmichannel Workflow N8N",
  "tags": [],
  "nodes": [
    {
      "id": "870cf631-17bf-45bf-a865-afcb775b48b6",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        736,
        1360
      ],
      "parameters": {
        "path": "8e947f01-96d1-47ed-bc4d-e28bcc71821a",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "32ac0aaf-aefd-4259-83af-c4730711c3f7",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        1360,
        1360
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a8ceb413-8575-4879-8849-ce6dd3831d01",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ Object.keys($json).length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "318e9acf-08ff-4087-b899-f9046bc55e32",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        896
      ],
      "parameters": {
        "content": "## the user exists\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4f624126-8d43-4161-a36b-eaf0a6a16b83",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        1856
      ],
      "parameters": {
        "content": "## the user does not exist"
      },
      "typeVersion": 1
    },
    {
      "id": "f124e921-285b-4933-b89c-9aad8f6dcfa8",
      "name": "Execute a SQL query",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1664,
        1632
      ],
      "parameters": {
        "query": "INSERT INTO onboarding_sessions (channel, platform_user_id, step)\nVALUES ($1, $2, 'waiting_for_email')\nON CONFLICT (channel, platform_user_id)\nDO NOTHING;",
        "options": {
          "queryReplacement": "={{ [\n  $json.channel ?? $node[\"Universal normalization\"].json.channel,\n  $json.platform_user_id ?? $node[\"Universal normalization\"].json.platform_user_id\n] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "b3bb2f0a-a796-4a3a-9b7a-8de5e6638165",
      "name": "If1",
      "type": "n8n-nodes-base.if",
      "position": [
        2256,
        1552
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "9184710a-ce1e-4938-80b9-cf97eb291012",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.step }}",
              "rightValue": "waiting_for_email"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "df899e6e-2829-4824-897a-b38ce6cd1341",
      "name": "waiting_for_otp",
      "type": "n8n-nodes-base.postgres",
      "position": [
        4016,
        288
      ],
      "parameters": {
        "query": "UPDATE onboarding_sessions\nSET step = 'waiting_for_otp'\nWHERE channel = $1\nAND platform_user_id = $2;",
        "options": {
          "queryReplacement": "={{ $node[\"Universal normalization\"].json.channel }}\n{{ $node[\"Universal normalization\"].json.platform_user_id }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "ba17533b-03ac-4216-a042-9fd10c113a77",
      "name": "If2- extracted_email",
      "type": "n8n-nodes-base.if",
      "position": [
        2928,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4c27bcc3-d6c0-4311-9a3e-770e7588975d",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $('Extract email').item.json.extracted_email }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "0b532844-bc77-429d-80c0-4d2b42656f87",
      "name": "Send email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        3792,
        288
      ],
      "parameters": {
        "html": "=<h2>Your verification code</h2>\n\n<p>Your code is:</p>\n\n<h1>{{ $('Generate OTP').item.json.otp_code }}</h1>\n\n<p>This code expires in 5 minutes.</p>\n\n<p>Este c\u00f3digo expira en 5 minutos.</p>",
        "options": {},
        "subject": "Tu c\u00f3digo de verificaci\u00f3n",
        "toEmail": "={{ $('Extract email').item.json.extracted_email }}",
        "fromEmail": "user@example.com"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "8f1b6e70-2b32-4ea7-aa0c-801e17f54c3f",
      "name": "If2",
      "type": "n8n-nodes-base.if",
      "position": [
        4240,
        288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "6c058e84-ffa0-4d4b-bac0-e94f57daa328",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.step }}",
              "rightValue": "waiting_for_otp"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "450bbb83-2d89-4236-8a5f-c122fcbd16b6",
      "name": "Execute a SQL query1",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2928,
        2080
      ],
      "parameters": {
        "query": "SELECT *\nFROM user_otps\nWHERE channel = $1\nAND platform_user_id = $2\nAND otp_code = $3\nAND is_used = false\nAND expires_at > now()\nORDER BY created_at DESC\nLIMIT 1;",
        "options": {
          "queryReplacement": "={{ [ $('Universal normalization').item.json.channel, $('Universal normalization').item.json.platform_user_id, (($('Universal normalization').item.json.message || '').trim()) ] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6,
      "alwaysOutputData": true
    },
    {
      "id": "9ac99506-f4f4-4913-9692-38392c909e26",
      "name": "Execute a SQL query2",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3920,
        2064
      ],
      "parameters": {
        "query": "UPDATE onboarding_sessions\nSET step = 'verified'\nWHERE channel = $1\nAND platform_user_id = $2;",
        "options": {
          "queryReplacement": "={{ [ $('Universal normalization').item.json.channel, $('Universal normalization').item.json.platform_user_id ] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "57cc8561-a7ad-4b68-bb16-2f9d640e61c5",
      "name": "IF_resend_otp",
      "type": "n8n-nodes-base.if",
      "position": [
        2720,
        1856
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "10e988ab-b186-4e6e-928a-20e5a00c7533",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ /^\\d{4,6}$/.test(($json.message || '').trim()) }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "5b96d50b-4395-4243-ab57-6a40cd2d3267",
      "name": "Execute a SQL query3",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1728,
        1248
      ],
      "parameters": {
        "query": "SELECT global_user_id, email\nFROM users\nWHERE global_user_id = $1\nLIMIT 1;",
        "options": {
          "queryReplacement": "={{ $json.global_user_id }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6,
      "alwaysOutputData": true
    },
    {
      "id": "a3eda303-0fd6-4507-b9a9-55846e0c0691",
      "name": "Execute a SQL query5",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3216,
        288
      ],
      "parameters": {
        "query": "UPDATE onboarding_sessions\nSET email = $1,\n    step = 'waiting_for_otp'\nWHERE channel = $2\nAND platform_user_id = $3;",
        "options": {
          "queryReplacement": "={{ [\n  $json.extracted_email,\n  $node[\"Universal normalization\"].json.channel,\n  $node[\"Universal normalization\"].json.platform_user_id\n] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "d5bfed53-4155-4210-8498-166d3de79e71",
      "name": "If4",
      "type": "n8n-nodes-base.if",
      "position": [
        2688,
        832
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f4260b1b-2e1d-470f-8d1d-b41751fe5a5d",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.extracted_email }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "b1465b3c-179e-4d80-b811-9de62413e731",
      "name": "IF_step- waiting_otp",
      "type": "n8n-nodes-base.if",
      "position": [
        2512,
        2144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "9d29eb8f-66ea-4d49-a4ac-d012bcc7c52b",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.step }}",
              "rightValue": "waiting_for_otp"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "924f50e9-8d46-4dd8-aeec-78a2fa213710",
      "name": "If email en session",
      "type": "n8n-nodes-base.if",
      "position": [
        4336,
        2064
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "489b46e1-5e8c-4a34-b764-c9190d2ac4a7",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.email }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "bf02cbaf-d67f-4038-a262-c0184606b8e5",
      "name": "Upsert user_channel",
      "type": "n8n-nodes-base.postgres",
      "position": [
        4848,
        2048
      ],
      "parameters": {
        "query": "INSERT INTO user_channels (global_user_id, channel, platform_user_id)\nVALUES ($1, $2, $3)\nON CONFLICT (channel, platform_user_id)\nDO UPDATE SET global_user_id = EXCLUDED.global_user_id\nRETURNING global_user_id, channel, platform_user_id;",
        "options": {
          "queryReplacement": "={{ [ $json.global_user_id, $('Universal normalization').item.json.channel, $('Universal normalization').item.json.platform_user_id ] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "a1e122d2-e5c9-4298-9bc3-9858f5f7030e",
      "name": "Universal normalization",
      "type": "n8n-nodes-base.code",
      "position": [
        944,
        1360
      ],
      "parameters": {
        "jsCode": "// Tomamos el body real del webhook\nconst data = $json.body || {};\n\nlet channel = null;\nlet platform_user_id = null;\nlet message = null;\n\n// Caso Telegram (simulado simple)\nif (data.message) {\n  channel = \"telegram\";\n  platform_user_id = data.message.from?.id?.toString();\n  message = data.message.text;\n}\n\n// Caso WhatsApp (Meta Cloud API estructura real)\nelse if (data.entry) {\n  channel = \"whatsapp\";\n  platform_user_id = data.entry[0].changes[0].value.messages[0].from;\n  message = data.entry[0].changes[0].value.messages[0].text?.body;\n}\n\n// Caso Email simple\nelse if (data.from && data.subject) {\n  channel = \"email\";\n  platform_user_id = data.from;\n  message = data.text || data.body;\n}\n\n// Fallback\nelse {\n  channel = \"unknown\";\n  platform_user_id = \"unknown\";\n  message = JSON.stringify(data);\n}\n\nreturn [{\n  channel,\n  platform_user_id,\n  message\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4cd10c7d-62f7-4b15-9480-f6e7c6577c31",
      "name": "Check if the channel already exists",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1152,
        1360
      ],
      "parameters": {
        "query": "SELECT global_user_id\nFROM user_channels\nWHERE channel = $1\nAND platform_user_id = $2;",
        "options": {
          "queryReplacement": "={{ $json.channel }} {{ $json.platform_user_id }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6,
      "alwaysOutputData": true
    },
    {
      "id": "2c85064f-863e-407c-827c-14922c94bbf9",
      "name": "Channel already verified",
      "type": "n8n-nodes-base.set",
      "position": [
        2000,
        1248
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "51e7da67-e385-4551-9558-a4d1ab99a67d",
              "name": "response_message",
              "type": "string",
              "value": "={{ `\u2705 Channel already verified for ${$json.email}. Continuamos con tu perfil.` }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "5211c7a7-472c-4da3-8cd1-06b766a18968",
      "name": "Search onboarding_session",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1984,
        1632
      ],
      "parameters": {
        "query": "SELECT step, email\nFROM onboarding_sessions\nWHERE channel = $1\nAND platform_user_id = $2;",
        "options": {
          "queryReplacement": "={{ [   $node[\"Universal normalization\"].json.channel,   $node[\"Universal normalization\"].json.platform_user_id ] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "fbb4941f-3b12-4871-8ab4-57db409d046a",
      "name": "Extract email",
      "type": "n8n-nodes-base.code",
      "position": [
        2480,
        832
      ],
      "parameters": {
        "jsCode": "const message = $node[\"Universal normalization\"].json.message;\n\n// Robust Regex to detect emails\nconst emailMatch = message\n  ? message.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i)\n  : null;\n\nreturn [{\n  ...$json,\n  extracted_email: emailMatch ? emailMatch[0].toLowerCase() : null\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "aba11586-756c-455d-a229-30ce2870366b",
      "name": "Greeting 1 - send your email",
      "type": "n8n-nodes-base.set",
      "position": [
        2800,
        1008
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "90cba22b-1b98-4e69-9a8f-27db89359e74",
              "name": "response_message",
              "type": "string",
              "value": "\ud83d\udc4b Welcome to Zyntha \u2615 To continue, I need your email address to create or verify your profile."
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "c8913fe3-9a74-49af-8bdf-94ba27ffb10d",
      "name": "Generate OTP",
      "type": "n8n-nodes-base.code",
      "position": [
        3440,
        288
      ],
      "parameters": {
        "jsCode": "const otp = Math.floor(100000 + Math.random() * 900000).toString();\n\nreturn [{\n  ...$json,\n  otp_code: otp,\n  otp_expires_at: new Date(Date.now() + 5 * 60 * 1000) // 5 minutos\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "647aa9aa-aecc-4836-9e83-c3d5d8c968ea",
      "name": "Save OTP",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3616,
        288
      ],
      "parameters": {
        "query": "INSERT INTO user_otps (channel, platform_user_id, otp_code, expires_at)\nVALUES ($1, $2, $3, $4);",
        "options": {
          "queryReplacement": "={{ $node[\"Universal normalization\"].json.channel }}\n{{ $node[\"Universal normalization\"].json.platform_user_id }}\n{{ $json.otp_code }}\n{{ $json.otp_expires_at }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "2cf8c2a4-0f5c-438c-9aeb-fb4d061506f7",
      "name": "response_message1 - verification code sent",
      "type": "n8n-nodes-base.set",
      "position": [
        4512,
        416
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "90cba22b-1b98-4e69-9a8f-27db89359e74",
              "name": "response_message",
              "type": "string",
              "value": "I sent you a verification code. Enter it here to continue."
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "b1f87e35-0a19-4331-8d65-eb6d8eab28cd",
      "name": "IF_code_exists?",
      "type": "n8n-nodes-base.if",
      "position": [
        3264,
        2080
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7acf0a8e-7ae4-4124-a27c-6ee01b140038",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.id }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "f9f76032-3329-4e84-9abd-cc7a09da59a2",
      "name": "code does not exist",
      "type": "n8n-nodes-base.set",
      "position": [
        3328,
        2464
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "90cba22b-1b98-4e69-9a8f-27db89359e74",
              "name": "response_message",
              "type": "string",
              "value": "\u274c Invalid code. Please try again with the correct code."
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "bcc811a6-d8ab-481e-9c1c-8fc92cb00d90",
      "name": "valid OTP",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3696,
        2064
      ],
      "parameters": {
        "query": "UPDATE user_otps\nSET is_used = true\nWHERE id = $1;",
        "options": {
          "queryReplacement": "={{ $('Execute a SQL query1').item.json.id }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "b9ef2797-fd43-4faf-bf9a-24eb24fd5c49",
      "name": "Get verified session email",
      "type": "n8n-nodes-base.postgres",
      "position": [
        4112,
        2064
      ],
      "parameters": {
        "query": "SELECT email\nFROM onboarding_sessions\nWHERE channel = $1\nAND platform_user_id = $2\nLIMIT 1;",
        "options": {
          "queryReplacement": "={{ [ $('Universal normalization').item.json.channel, $('Universal normalization').item.json.platform_user_id ] }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6,
      "alwaysOutputData": true
    },
    {
      "id": "514fc91c-ad13-461a-b77e-07eb259bdb91",
      "name": "Missing email response",
      "type": "n8n-nodes-base.set",
      "position": [
        4592,
        2400
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "7616b83a-63f1-41b4-a625-9db26036d52e",
              "name": "response_message",
              "type": "string",
              "value": "\u26a0\ufe0f I couldn\u2019t find the session email. Please send your email to continue."
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "fb6eb345-fe1b-4898-8b51-4b7a2ad43cf4",
      "name": "Upsert user by email",
      "type": "n8n-nodes-base.postgres",
      "position": [
        4576,
        2048
      ],
      "parameters": {
        "query": "INSERT INTO users (email, verified)\nVALUES ($1, true)\nON CONFLICT (email)\nDO UPDATE SET verified = true, updated_at = now()\nRETURNING global_user_id, email;",
        "options": {
          "queryReplacement": "={{ $json.email }}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "8d44f896-f774-4853-a82d-ad5be67b13b0",
      "name": "Verification completed response",
      "type": "n8n-nodes-base.set",
      "position": [
        5152,
        2048
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "7a0603f0-8c66-4e82-abcd-5c797a46301a",
              "name": "response_message",
              "type": "string",
              "value": "\u2705 Verification completed. This channel has been linked to your user."
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "922a8ec9-3581-421e-973d-1209bfa0f4fb",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        1136
      ],
      "parameters": {
        "width": 544,
        "height": 608,
        "content": "## Omnichannel OTP Verification & User Onboarding\n\n### How it works\nThis workflow centralizes user verification across WhatsApp, Telegram, and email using OTP validation.\n\nIt detects the channel, manages onboarding sessions, generates secure OTP codes, and links verified channels to a single global user identity.\n\n### How to set up\n1. Import the workflow into n8n.\n2. Configure Postgres or Supabase credentials.\n3. Download and create the required database tables (users, user_channels, user_otps, onboarding_sessions).\n4. Connect email and messaging channel credentials.\n5. Test with new and returning users.\n\n### Customization\n\u2022 Adjust OTP expiration time.\n\u2022 Modify onboarding steps or validation logic.\n\u2022 Add additional communication channels."
      },
      "typeVersion": 1
    },
    {
      "id": "6c92564b-02cf-47a8-9881-4ce41dd45f2c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        720,
        1152
      ],
      "parameters": {
        "color": 7,
        "width": 752,
        "height": 512,
        "content": "## Section 1 \u2014 Channel Detection & Normalization\n\nReceives incoming requests from multiple platforms and identifies the communication channel.\n\nNormalizes user data into a unified structure before checking if the channel is already linked."
      },
      "typeVersion": 1
    },
    {
      "id": "55666d14-9851-4b68-b504-34c708dcc145",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        1024
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 384,
        "content": "## Section 2 \u2014 Existing Channel Validation\n\n- Checks if the incoming channel is already associated with a verified user.\n\n- If found, skips the verification process and continues with the existing profile."
      },
      "typeVersion": 1
    },
    {
      "id": "0c7ce79c-bc41-447b-a17c-f8e2434796a8",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        1472
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 384,
        "content": "## Section 3 \u2014 Session Initialization\n\n- Creates or retrieves the onboarding session for the current channel.\n\n- Tracks the current step of the verification process."
      },
      "typeVersion": 1
    },
    {
      "id": "0c099410-84a3-46c3-95a2-5671b2bc88f1",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2304,
        576
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 608,
        "content": "## Section 4 \u2014 Email Extraction & Validation\n\n- Extracts a valid email from the user message using regex detection.\n\n- If no email is found, prompts the user to provide one before continuing."
      },
      "typeVersion": 1
    },
    {
      "id": "b8f1b027-e7b3-401d-86c9-45a26752a4cd",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3152,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 1568,
        "height": 576,
        "content": "## Section 5 \u2014 OTP Generation & Delivery\n\n- Generates a secure one-time password and stores it with an expiration timestamp.\n\n- Sends the OTP via email and updates the session to waiting_for_otp.\n\n- A confirmation message is sent to the email used for delivering the OTP."
      },
      "typeVersion": 1
    },
    {
      "id": "37e80802-135a-419c-968c-23ed5c222b4e",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2480,
        1648
      ],
      "parameters": {
        "color": 7,
        "width": 1088,
        "height": 1072,
        "content": "## Section 4 \u2014 Email Extraction & Validation\n\n- Extracts a valid email from the user message using regex detection.\n\n- If no email is found, prompts the user to provide one before continuing.\n\n- A message is sent to the user if the code does not exist, asking them to verify it and send it again."
      },
      "typeVersion": 1
    },
    {
      "id": "3b112758-2051-4a1d-85b7-cd5ebf9210a8",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3664,
        1728
      ],
      "parameters": {
        "color": 7,
        "width": 1760,
        "height": 976,
        "content": "## Section 7 \u2014 Identity Consolidation\n\n- Marks the OTP as used and updates the session as verified in the Supabase database.\n\n- Links the verified channel to a single global user ID to ensure omnichannel continuity.\n\n- A confirmation message is sent via email if the verification has been completed and successfully linked to the user."
      },
      "typeVersion": 1
    },
    {
      "id": "fac105f0-454d-4905-8d2d-5322c67b897e",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3792,
        2880
      ],
      "parameters": {
        "color": 3,
        "width": 544,
        "height": 272,
        "content": "## DATABASE REQUIRED\n\nThis workflow depends on predefined Postgres tables.\n\nYou must create users, user_channels, user_otps, and onboarding_sessions before running the flow."
      },
      "typeVersion": 1
    },
    {
      "id": "5c4adad1-c940-427d-a7d5-1d3b6eba74e3",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3408,
        -272
      ],
      "parameters": {
        "color": 3,
        "width": 304,
        "height": 208,
        "content": "## Security Notice\n- Do not log OTP codes in production environments.\n\n- Always define an expiration time and mark codes as used after validation."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "02aace02-c27d-4c6b-bde2-d9df86bef1fc",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Execute a SQL query3",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Execute a SQL query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If1": {
      "main": [
        [
          {
            "node": "Extract email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF_step- waiting_otp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If2": {
      "main": [
        [],
        [
          {
            "node": "response_message1 - verification code sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If4": {
      "main": [
        [
          {
            "node": "If2- extracted_email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Greeting 1 - send your email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Universal normalization",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save OTP": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "valid OTP": {
      "main": [
        [
          {
            "node": "Execute a SQL query2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send email": {
      "main": [
        [
          {
            "node": "waiting_for_otp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate OTP": {
      "main": [
        [
          {
            "node": "Save OTP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract email": {
      "main": [
        [
          {
            "node": "If4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_resend_otp": {
      "main": [
        [],
        [
          {
            "node": "Execute a SQL query1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_code_exists?": {
      "main": [
        [
          {
            "node": "valid OTP",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "code does not exist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "waiting_for_otp": {
      "main": [
        [
          {
            "node": "If2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query": {
      "main": [
        [
          {
            "node": "Search onboarding_session",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If email en session": {
      "main": [
        [
          {
            "node": "Upsert user by email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Missing email response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert user_channel": {
      "main": [
        [
          {
            "node": "Verification completed response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query1": {
      "main": [
        [
          {
            "node": "IF_code_exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query2": {
      "main": [
        [
          {
            "node": "Get verified session email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query3": {
      "main": [
        [
          {
            "node": "Channel already verified",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query5": {
      "main": [
        [
          {
            "node": "Generate OTP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_step- waiting_otp": {
      "main": [
        [
          {
            "node": "IF_resend_otp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If2- extracted_email": {
      "main": [
        [
          {
            "node": "Execute a SQL query5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert user by email": {
      "main": [
        [
          {
            "node": "Upsert user_channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Universal normalization": {
      "main": [
        [
          {
            "node": "Check if the channel already exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search onboarding_session": {
      "main": [
        [
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get verified session email": {
      "main": [
        [
          {
            "node": "If email en session",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if the channel already exists": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}