{
    "name": "Fluss → your CRM (starter)",
    "nodes": [
        {
            "parameters": {
                "httpMethod": "POST",
                "path": "fluss-lead",
                "responseMode": "lastNode",
                "options": {}
            },
            "id": "webhook-trigger",
            "name": "Fluss Webhook",
            "type": "n8n-nodes-base.webhook",
            "typeVersion": 2,
            "position": [-220, 0],
            "webhookId": "fluss-lead"
        },
        {
            "parameters": {
                "mode": "runOnceForEachItem",
                "jsCode": "// Verify the request really came from Fluss.\n//\n// Fluss signs every delivery with HMAC-SHA256 over `<timestamp>.<rawBody>`\n// and sends it as `x-fluss-signature: t=<unix>,v1=<hex>`. Without this check\n// anyone who learns your webhook URL can post fake leads into your CRM.\n//\n// Set FLUSS_WEBHOOK_SECRET to the signing secret from the automation\n// template in Fluss (Automations → your custom integration → Signing secret).\n\nconst crypto = require('crypto');\n\nconst secret = $env.FLUSS_WEBHOOK_SECRET;\nif (!secret) {\n  throw new Error('FLUSS_WEBHOOK_SECRET is not set on this n8n instance');\n}\n\nconst headers = $input.item.json.headers ?? {};\nconst signature = headers['x-fluss-signature'];\nif (!signature) {\n  throw new Error('Missing x-fluss-signature header');\n}\n\nconst parts = Object.fromEntries(\n  signature.split(',').map((p) => {\n    const [k, ...rest] = p.trim().split('=');\n    return [k, rest.join('=')];\n  })\n);\n\nconst timestamp = Number(parts.t);\nif (!Number.isFinite(timestamp)) {\n  throw new Error('Malformed signature timestamp');\n}\n\n// Reject replays. Fluss recommends a 5 minute window.\nconst ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestamp);\nif (ageSeconds > 300) {\n  throw new Error(`Signature too old (${ageSeconds}s)`);\n}\n\n// IMPORTANT: sign the RAW body exactly as sent. Re-serialising the parsed\n// JSON can reorder keys and the signature will not match. Enable \"Raw Body\"\n// on the Webhook node if you hit mismatches.\nconst rawBody =\n  typeof $input.item.json.body === 'string'\n    ? $input.item.json.body\n    : JSON.stringify($input.item.json.body);\n\nconst expected = crypto\n  .createHmac('sha256', secret)\n  .update(`${timestamp}.${rawBody}`)\n  .digest('hex');\n\nconst a = Buffer.from(expected, 'hex');\nconst b = Buffer.from(parts.v1 ?? '', 'hex');\n\nif (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {\n  throw new Error('Invalid signature');\n}\n\nreturn {\n  json:\n    typeof $input.item.json.body === 'string'\n      ? JSON.parse($input.item.json.body)\n      : $input.item.json.body\n};"
            },
            "id": "verify-signature",
            "name": "Verify Signature",
            "type": "n8n-nodes-base.code",
            "typeVersion": 2,
            "position": [0, 0]
        },
        {
            "parameters": {
                "content": "## Start here\n\n1. Copy this workflow's **Production** webhook URL from the *Fluss Webhook* node.\n2. In Fluss: **Automations → your custom integration** — paste the URL, generate a **Signing secret**, save.\n3. On this n8n instance set the env var `FLUSS_WEBHOOK_SECRET` to that same secret.\n4. Replace the *Your CRM* node with whatever you actually use.\n\n### Writing back into Fluss\n\nThe *Comment back to Fluss* node shows the pattern. Create an API key in\nFluss under **Account → API keys** with the `comments:write` scope and set it\nas `FLUSS_API_KEY`.\n\nThe key is scoped to your account only and can be revoked at any time.\n\n### Payload\n\nSee https://fluss.ai/docs/webhooks for the full event shape.",
                "height": 460,
                "width": 420
            },
            "id": "sticky-setup",
            "name": "Setup",
            "type": "n8n-nodes-base.stickyNote",
            "typeVersion": 1,
            "position": [-700, -140]
        },
        {
            "parameters": {
                "conditions": {
                    "options": {
                        "caseSensitive": true,
                        "version": 2
                    },
                    "conditions": [
                        {
                            "id": "has-email",
                            "leftValue": "={{ $json.lead.email }}",
                            "rightValue": "",
                            "operator": {
                                "type": "string",
                                "operation": "notEmpty",
                                "singleValue": true
                            }
                        }
                    ],
                    "combinator": "and"
                },
                "options": {}
            },
            "id": "has-contact",
            "name": "Has Contact?",
            "type": "n8n-nodes-base.if",
            "typeVersion": 2,
            "position": [220, 0]
        },
        {
            "parameters": {
                "assignments": {
                    "assignments": [
                        {
                            "id": "name",
                            "name": "name",
                            "value": "={{ [$json.lead.firstName, $json.lead.lastName].filter(Boolean).join(' ') }}",
                            "type": "string"
                        },
                        {
                            "id": "email",
                            "name": "email",
                            "value": "={{ $json.lead.email }}",
                            "type": "string"
                        },
                        {
                            "id": "phone",
                            "name": "phone",
                            "value": "={{ $json.lead.phone }}",
                            "type": "string"
                        },
                        {
                            "id": "address",
                            "name": "address",
                            "value": "={{ [[$json.lead.street, $json.lead.streetNumber].filter(Boolean).join(' '), [$json.lead.postalCode, $json.lead.city].filter(Boolean).join(' ')].filter(Boolean).join(', ') }}",
                            "type": "string"
                        },
                        {
                            "id": "estimatedValue",
                            "name": "estimatedValue",
                            "value": "={{ $json.lead.value }}",
                            "type": "number"
                        },
                        {
                            "id": "valuationPdf",
                            "name": "valuationPdf",
                            "value": "={{ $json.lead.valuationUrl }}",
                            "type": "string"
                        },
                        {
                            "id": "flussLeadId",
                            "name": "flussLeadId",
                            "value": "={{ $json.lead.hashId }}",
                            "type": "string"
                        }
                    ]
                },
                "options": {}
            },
            "id": "map-fields",
            "name": "Map To CRM Fields",
            "type": "n8n-nodes-base.set",
            "typeVersion": 3.4,
            "position": [440, -100]
        },
        {
            "parameters": {
                "content": "## Replace me\n\nSwap this for your CRM's own node —\nHubSpot, Pipedrive, onOffice, Propstack,\nor a plain HTTP Request.\n\n⚠️ onOffice answers **HTTP 200 with the\nerror inside the body**. Check the body,\nnot just the status, or dead pushes will\nlook like successes.",
                "height": 260,
                "width": 300,
                "color": 3
            },
            "id": "sticky-crm",
            "name": "Replace me",
            "type": "n8n-nodes-base.stickyNote",
            "typeVersion": 1,
            "position": [640, -260]
        },
        {
            "parameters": {
                "method": "POST",
                "url": "https://example-crm.test/api/contacts",
                "sendBody": true,
                "specifyBody": "json",
                "jsonBody": "={{ JSON.stringify($json) }}",
                "options": {}
            },
            "id": "crm-push",
            "name": "Your CRM",
            "type": "n8n-nodes-base.httpRequest",
            "typeVersion": 4.2,
            "position": [660, -100]
        },
        {
            "parameters": {
                "method": "POST",
                "url": "https://fluss.ai/api/trpc/external.addComment",
                "sendHeaders": true,
                "headerParameters": {
                    "parameters": [
                        {
                            "name": "x-fluss-api-key",
                            "value": "={{ $env.FLUSS_API_KEY }}"
                        },
                        {
                            "name": "x-raw-output",
                            "value": "1"
                        }
                    ]
                },
                "sendBody": true,
                "specifyBody": "json",
                "jsonBody": "={{ JSON.stringify({ json: { leadHashId: $('Verify Signature').item.json.lead.hashId, text: 'Pushed to CRM by n8n.' } }) }}",
                "options": {}
            },
            "id": "comment-back",
            "name": "Comment back to Fluss",
            "type": "n8n-nodes-base.httpRequest",
            "typeVersion": 4.2,
            "position": [880, -100]
        },
        {
            "parameters": {
                "assignments": {
                    "assignments": [
                        {
                            "id": "skipped",
                            "name": "skipped",
                            "value": "lead has no email address",
                            "type": "string"
                        }
                    ]
                },
                "options": {}
            },
            "id": "skip",
            "name": "Skip",
            "type": "n8n-nodes-base.set",
            "typeVersion": 3.4,
            "position": [440, 120]
        }
    ],
    "connections": {
        "Fluss Webhook": {
            "main": [[{ "node": "Verify Signature", "type": "main", "index": 0 }]]
        },
        "Verify Signature": {
            "main": [[{ "node": "Has Contact?", "type": "main", "index": 0 }]]
        },
        "Has Contact?": {
            "main": [
                [{ "node": "Map To CRM Fields", "type": "main", "index": 0 }],
                [{ "node": "Skip", "type": "main", "index": 0 }]
            ]
        },
        "Map To CRM Fields": {
            "main": [[{ "node": "Your CRM", "type": "main", "index": 0 }]]
        },
        "Your CRM": {
            "main": [
                [{ "node": "Comment back to Fluss", "type": "main", "index": 0 }]
            ]
        }
    },
    "settings": {
        "executionOrder": "v1"
    },
    "pinData": {},
    "meta": {
        "templateCredsSetupCompleted": true
    }
}
