{
  "meta": {
    "description": "When a lead is created, verify HMAC, ack, enrich via GET /v1/leads/:id, notify Slack.",
    "templateCredsSetupCompleted": false
  },
  "name": "Zivvy CRM \u2014 New Lead \u2192 Slack",
  "tags": [
    {
      "name": "zivvy"
    },
    {
      "name": "crm"
    },
    {
      "name": "leads"
    }
  ],
  "nodes": [
    {
      "id": "33d37d60-4280-4e36-bd8e-1aa74360dac8",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        16
      ],
      "parameters": {
        "width": 480,
        "height": 880,
        "content": "## Zivvy CRM \u2014 New Lead \u2192 Slack\n\n### How it works\n\nThis workflow receives new-lead webhook events from Zivvy CRM and validates their signature before doing anything else. Authenticated events are checked against the expected event type, while bad signatures are stopped and unrelated events are ignored. Matching lead events are formatted into a Slack message, with a nearby optional Zivvy API enrichment request, and then sent to Slack through a webhook URL.\n\n### Setup steps\n\n- Configure the Zivvy webhook in Zivvy CRM to point to the n8n webhook URL generated by the \"Zivvy Webhook\" node.\n- Set the shared Zivvy signing secret or verification parameters used by the \"Verify Zivvy Signature\" code node.\n- Review the \"Signature OK?\" and \"Event Match?\" conditions so they match Zivvy's payload structure and the specific new-lead event name.\n- Set the SLACK_WEBHOOK_URL environment variable to a valid Slack incoming webhook URL.\n- Configure any required Zivvy API authentication for the \"Enrich from Zivvy API\" request if that enrichment branch is used.\n\n### Customization\n\nAdjust the fields in \"Build Notification\" to change the Slack message text, channel routing metadata, or included lead details. Update the event filter if you want to notify Slack for additional Zivvy CRM event types."
      },
      "typeVersion": 1
    },
    {
      "id": "4cbbbc1f-4563-4f39-a120-14e61fa10a86",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 304,
        "content": "## Receive and verify webhook\n\nThis left-side intake cluster receives the Zivvy webhook, runs custom signature verification, and branches based on whether the request is authentic."
      },
      "typeVersion": 1
    },
    {
      "id": "c14251c5-24f3-4810-8c49-661fa57526ac",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 592,
        "content": "## Filter invalid events\n\nThis central branching cluster handles requests that should not continue: bad signatures are stopped with an error, and unmatched Zivvy events are skipped. It also contains the event-type decision point that routes valid matching leads onward."
      },
      "typeVersion": 1
    },
    {
      "id": "17ca279d-6290-465f-b2c6-4b588ef27d8a",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1440,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 496,
        "content": "## Enrich and notify Slack\n\nThis right-side output cluster prepares the Slack notification, optionally calls the Zivvy API for lead enrichment, and posts the formatted message to Slack via an incoming webhook."
      },
      "typeVersion": 1
    },
    {
      "id": "c9af3951-2923-44da-805e-5f7a37080e8b",
      "name": "When New Lead in Zivvy",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        360
      ],
      "parameters": {
        "path": "zivvy/crm-leads",
        "options": {
          "rawBody": true,
          "ignoreBots": true,
          "responseCode": 200
        },
        "httpMethod": "POST",
        "responseData": "firstEntryJson",
        "responseMode": "onReceived"
      },
      "typeVersion": 2
    },
    {
      "id": "eec962ba-4da0-46ae-9d99-32ff1aecde31",
      "name": "Validate Zivvy Signature",
      "type": "n8n-nodes-base.code",
      "position": [
        500,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "/**\n * Verify Zivvy webhook HMAC.\n * Production (zivvy_brand/api/webhooks.py):\n *   signature = HMAC_SHA256(secret, raw_body_bytes).hexdigest()\n *   header    = \"sha256=\" + signature\n *\n * Set env ZIVVY_WEBHOOK_SECRET (n8n \u2192 Settings \u2192 Variables)\n * or edit SECRET below for local tests only.\n *\n * Docs: https://docs.n8n.io/code/code-node/\n *       https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/\n */\nconst crypto = require('crypto');\n\nconst SECRET = (typeof $env !== 'undefined' && $env.ZIVVY_WEBHOOK_SECRET)\n  ? $env.ZIVVY_WEBHOOK_SECRET\n  : 'CHANGE_ME_ZIVVY_WEBHOOK_SECRET';\n\nif (!SECRET || SECRET.startsWith('CHANGE_ME')) {\n  throw new Error('Set n8n variable ZIVVY_WEBHOOK_SECRET to your Zivvy webhook signing secret.');\n}\n\nconst item = $input.first();\nconst headers = item.json.headers || {};\nconst lower = {};\nfor (const [k, v] of Object.entries(headers)) lower[String(k).toLowerCase()] = v;\n\nconst sigHeader = String(lower['x-zivvy-signature'] || '');\nconst eventHeader = String(lower['x-zivvy-event'] || '');\nconst deliveryHeader = String(lower['x-zivvy-delivery'] || '');\n\nlet rawBody = '';\nif (item.binary && item.binary.data) {\n  const bin = item.binary.data;\n  rawBody = Buffer.from(bin.data, bin.encoding || 'base64').toString('utf8');\n} else if (typeof item.json.body === 'string') {\n  rawBody = item.json.body;\n} else if (item.json.body && typeof item.json.body === 'object') {\n  // Compact re-serialize \u2014 matches Python json.dumps(..., separators=(',', ':'))\n  rawBody = JSON.stringify(item.json.body);\n} else {\n  // Some n8n versions flatten the body onto json\n  const { headers: _h, params: _p, query: _q, webhookUrl: _w, ...rest } = item.json;\n  rawBody = JSON.stringify(rest);\n}\n\nconst expected = 'sha256=' + crypto\n  .createHmac('sha256', SECRET)\n  .update(rawBody, 'utf8')\n  .digest('hex');\n\nconst a = Buffer.from(sigHeader);\nconst b = Buffer.from(expected);\nconst ok = a.length === b.length && crypto.timingSafeEqual(a, b);\n\nlet payload;\ntry {\n  payload = JSON.parse(rawBody);\n} catch {\n  payload = item.json.body || item.json;\n}\n\nif (!ok) {\n  return [{\n    json: {\n      signature_valid: false,\n      error: 'invalid_signature',\n      event: eventHeader || payload?.event,\n      delivery_id: deliveryHeader\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    signature_valid: true,\n    event: eventHeader || payload.event,\n    delivery_id: deliveryHeader,\n    resource: payload.resource,\n    timestamp: payload.timestamp,\n    data: payload.data || {},\n    payload\n  }\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "13537756-b5a8-4c3b-9028-9ee0ad927f66",
      "name": "If Signature Valid",
      "type": "n8n-nodes-base.if",
      "position": [
        760,
        360
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "59c4c462-f9ac-4189-88c8-e9e2e67576ca",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.signature_valid }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "260a097a-37d0-4ba0-afe8-b15854ad0d5e",
      "name": "Stop on Invalid Signature",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        1000,
        560
      ],
      "parameters": {
        "errorMessage": "Zivvy webhook signature invalid"
      },
      "typeVersion": 1
    },
    {
      "id": "ca8fa5ac-8ecd-4a34-905d-cb773f80918f",
      "name": "Check Event Type",
      "type": "n8n-nodes-base.if",
      "position": [
        1000,
        360
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "a4ac3537-d729-4fb9-9afa-3e32ca753b0b",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.event }}",
              "rightValue": "leads.created"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5c2a229b-6115-495a-a067-269f3e3729eb",
      "name": "Ignore Unmatched Events",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1240,
        560
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "9e22f5b6-2284-42ed-a11d-22ad5b9ce127",
      "name": "Set Slack Notification Data",
      "type": "n8n-nodes-base.set",
      "position": [
        1480,
        360
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "6c7ec1e4-af92-4328-bd32-e60c89d5043c",
              "name": "text",
              "type": "string",
              "value": "={{ '\u2022 *' + $json.event + '*\\n\u2022 Doc: `' + ($json.data.name || '') + '`\\n\u2022 Customer/Supplier: ' + ($json.data.customer || $json.data.supplier || '\u2014') + '\\n\u2022 Employee: ' + ($json.data.employee || '\u2014') + '\\n\u2022 Status: ' + ($json.data.status || '\u2014') + '\\n\u2022 Total: ' + ($json.data.grand_total || '\u2014') + '\\n\u2022 Area: CRM' }}"
            },
            {
              "id": "35bf6a4b-19e2-40ef-aff4-01970da51a95",
              "name": "area",
              "type": "string",
              "value": "CRM"
            }
          ]
        },
        "duplicateItem": false
      },
      "typeVersion": 3.4
    },
    {
      "id": "65e94aba-4295-46e3-87fb-99280634bb15",
      "name": "Send to Slack Webhook",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1720,
        360
      ],
      "parameters": {
        "url": "={{ $env.SLACK_WEBHOOK_URL }}",
        "method": "POST",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={{ { text: $json.text, blocks: $json.blocks || undefined } }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "9f5fd139-39a7-411f-844f-064f4c1fc3e3",
      "name": "Fetch Lead Details from Zivvy",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1480,
        200
      ],
      "parameters": {
        "url": "={{ 'https://api.zivvy.xyz/v1/' + ('leads') + '/' + encodeURIComponent($json.data.name) }}",
        "method": "GET",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    }
  ],
  "active": false,
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": "",
    "executionOrder": "v1"
  },
  "versionId": "39c921f5-267a-410a-ad01-bab95a62f870",
  "connections": {
    "Check Event Type": {
      "main": [
        [
          {
            "node": "Set Slack Notification Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Lead Details from Zivvy",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ignore Unmatched Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Signature Valid": {
      "main": [
        [
          {
            "node": "Check Event Type",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop on Invalid Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When New Lead in Zivvy": {
      "main": [
        [
          {
            "node": "Validate Zivvy Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Zivvy Signature": {
      "main": [
        [
          {
            "node": "If Signature Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Slack Notification Data": {
      "main": [
        [
          {
            "node": "Send to Slack Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}