AutomationFlowsMarketing & Ads › Secure Web Form to Odoo CRM Lead Creation with Utm Tracking

Secure Web Form to Odoo CRM Lead Creation with Utm Tracking

ByGaetano Castaldo @castaldosolutions on n8n.io

Create records in Odoo from any webform via a secure webhook. The workflow validates required fields, resolves UTMs by name (source, medium, campaign) and writes standard lead fields in Odoo. Clean, portable, and production-ready. ✅ Secure Webhook with Header Auth () ✅ Required…

Webhook trigger★★★★☆ complexity19 nodesOdooExecution Data
Marketing & Ads Trigger: Webhook Nodes: 19 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #7289 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "LYAnJfzpvewwxal6",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Send Lead from Web to Odoo",
  "tags": [],
  "nodes": [
    {
      "id": "87228205-1f02-474e-a99d-03471805e0d3",
      "name": "Webhook - Lead Webform",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -3344,
        560
      ],
      "parameters": {
        "path": "lead-webform",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "27f055d9-7e29-4acd-8d4b-de356b611355",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        -1872,
        544
      ],
      "parameters": {
        "numberInputs": 4
      },
      "typeVersion": 3.2,
      "alwaysOutputData": true
    },
    {
      "id": "dc8702e9-c4c6-487e-a5e0-19a2dda4ad94",
      "name": "Create Lead",
      "type": "n8n-nodes-base.odoo",
      "position": [
        -1504,
        544
      ],
      "parameters": {
        "resource": "custom",
        "customResource": "crm.lead",
        "fieldsToCreateOrUpdate": {
          "fields": [
            {
              "fieldName": "name",
              "fieldValue": "={{ $json.contact_name }}"
            },
            {
              "fieldName": "phone",
              "fieldValue": "={{ $json.phone }}"
            },
            {
              "fieldName": "email_from",
              "fieldValue": "={{ $json.email_from }}"
            },
            {
              "fieldName": "description",
              "fieldValue": "={{ $json.description }}"
            },
            {
              "fieldName": "source_id",
              "fieldValue": "={{ $json.source_id }}"
            },
            {
              "fieldName": "medium_id",
              "fieldValue": "={{ $json.medium_id }}"
            },
            {
              "fieldName": "campaign_id",
              "fieldValue": "={{ $json.campaign_id }}"
            },
            {
              "fieldName": "type",
              "fieldValue": "={{ $json.type }}"
            },
            {
              "fieldName": "contact_name",
              "fieldValue": "={{ $json.contact_name }}"
            }
          ]
        }
      },
      "credentials": {
        "odooApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1c7b3f98-b467-4218-934c-c34ab2efebc8",
      "name": "Prepare Request",
      "type": "n8n-nodes-base.code",
      "position": [
        -1696,
        528
      ],
      "parameters": {
        "jsCode": "// n8n Code node (Run once for all items)\n//\n// Purpose\n// -------\n// Merge the four inputs coming from the previous Merge node into a single,\n// clean JSON ready for Odoo `crm.lead` create.\n//\n// Expected inputs (from Merge):\n// - 1 item from the webhook: { body: { firstname, lastname, email, phone, notes, ... } }\n//   (or, in some setups, those fields may be at the root instead of under `body`)\n// - 1 item with { source_id }\n// - 1 item with { medium_id }\n// - 1 item with { campaign_id }\n//\n// Output:\n// - 1 item: { name, contact_name, email_from, phone, description, type, campaign_id, source_id, medium_id }\n\nfunction s(x) { return (x ?? '').toString().trim(); }\n\nlet payload = null;\nlet source_id = null;\nlet medium_id = null;\nlet campaign_id = null;\n\n// Scan all items coming from Merge and collect what we need\nfor (const it of items) {\n  const j = it.json ?? it;\n\n  // Identify the webhook item.\n  // Prefer `j.body` (typical webhook shape), but fall back to root-level fields just in case.\n  if (j.body || j.firstname || j.lastname || j.email || j.notes || j.phone) {\n    payload = j.body ?? j;\n  }\n\n  // Collect UTM IDs (take the first defined value seen for each)\n  if (j.source_id !== undefined && j.source_id !== null && source_id === null) {\n    const n = Number(j.source_id);\n    source_id = Number.isFinite(n) ? n : null;\n  }\n  if (j.medium_id !== undefined && j.medium_id !== null && medium_id === null) {\n    const n = Number(j.medium_id);\n    medium_id = Number.isFinite(n) ? n : null;\n  }\n  if (j.campaign_id !== undefined && j.campaign_id !== null && campaign_id === null) {\n    const n = Number(j.campaign_id);\n    campaign_id = Number.isFinite(n) ? n : null;\n  }\n}\n\n// Safety check: if the webhook item is missing, fail early with a helpful message\nif (!payload) {\n  throw new Error('Webhook payload not found after Merge. Ensure the IF(false) branch is connected to the Merge.');\n}\n\n// Build the final lead payload for Odoo\nconst first = s(payload.firstname);\nconst last  = s(payload.lastname);\nconst fullName = `${first} ${last}`.trim();\n\nconst out = {\n  // Core lead fields\n  name: fullName || s(payload.email) || 'New Lead',\n  contact_name: fullName,\n  email_from: s(payload.email),\n  phone: s(payload.phone),\n  description: s(payload.notes),\n  type: 'lead',\n\n  // UTM Many2one IDs (nullable if not provided/found)\n  campaign_id,\n  source_id,\n  medium_id,\n};\n\nreturn [{ json: out }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6299f436-0481-4291-8b70-e62555cfb76b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        416
      ],
      "parameters": {
        "color": 4,
        "width": 816,
        "height": 384,
        "content": "## Odoo - Lead Create \nPreparation of the request creating the right body for Lead custom object"
      },
      "typeVersion": 1
    },
    {
      "id": "1bb410d2-86d5-458a-86bb-a5b6a9938df2",
      "name": "Execution Data",
      "type": "n8n-nodes-base.executionData",
      "position": [
        -1296,
        544
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "b3c48a34-824e-471a-bea7-a5a1f4ac0933",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3392,
        240
      ],
      "parameters": {
        "color": 5,
        "width": 400,
        "height": 544,
        "content": "## Webhook linked to different sources \nExample:\n{\n    \"firstname\" : \"John\",\n    \"lastname\" : \"Doe\",\n    \"phone\" : \"+393331212123\",\n    \"email\" : \"test@outlook.com\",\n    \"source\" : \"Ads\",\n    \"medium\" : \"Website\",\n    \"notes\" : \"test notes\",\n    \"campaign\" : \"WebMassive\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "b4cfdde2-570e-4e0f-bd2f-7f69bfc5fa65",
      "name": "UTM: Get Source ID",
      "type": "n8n-nodes-base.odoo",
      "onError": "continueRegularOutput",
      "position": [
        -2704,
        176
      ],
      "parameters": {
        "limit": 1,
        "options": {
          "fieldsList": [
            "name",
            "id"
          ]
        },
        "resource": "custom",
        "operation": "getAll",
        "filterRequest": {
          "filter": [
            {
              "value": "={{ $json.body.source }}",
              "fieldName": "name"
            }
          ]
        },
        "customResource": "utm.source"
      },
      "credentials": {
        "odooApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "2db2e52d-ff07-4bf0-8f69-7ff99e39a3c4",
      "name": "UTM: Get Medium ID",
      "type": "n8n-nodes-base.odoo",
      "onError": "continueRegularOutput",
      "position": [
        -2704,
        304
      ],
      "parameters": {
        "limit": 1,
        "options": {
          "fieldsList": [
            "name",
            "id"
          ]
        },
        "resource": "custom",
        "operation": "getAll",
        "filterRequest": {
          "filter": [
            {
              "value": "={{ $json.body.medium }}",
              "fieldName": "name"
            }
          ]
        },
        "customResource": "utm.medium"
      },
      "credentials": {
        "odooApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "232bdab3-82c5-454a-866e-4ddb90eea4d7",
      "name": "UTM: Get Campaign ID",
      "type": "n8n-nodes-base.odoo",
      "onError": "continueRegularOutput",
      "position": [
        -2704,
        432
      ],
      "parameters": {
        "limit": 1,
        "options": {
          "fieldsList": [
            "name",
            "id"
          ]
        },
        "resource": "custom",
        "operation": "getAll",
        "filterRequest": {
          "filter": [
            {
              "value": "={{ $json.body.campaign }}",
              "fieldName": "name"
            }
          ]
        },
        "customResource": "utm.campaign"
      },
      "credentials": {
        "odooApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "569cdaf8-08aa-469d-a37e-c876359cc121",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4544,
        240
      ],
      "parameters": {
        "width": 512,
        "height": 672,
        "content": "## How To Use\n\n### Prerequisites\n- **Odoo**: enable Leads (CRM \u2192 Settings \u2192 Leads).\n- **Odoo API Key** for your user (use it as the *password*).\n- **n8n Odoo credentials**: URL, DB name, Login, **API Key**.\n- **Public URL** for the webhook (ngrok/Cloudflare/reverse proxy). Make sure WEBHOOK_URL / N8N_HOST / N8N_PROTOCOL / N8N_PORT are consistent.\n- **Header Auth**: set a secret, e.g. header `x-webhook-token: {{$env.WEBHOOK_SECRET}}`.\n\n### Payload (JSON)\n- **Required**: `firstname`, `lastname`, `email`\n- **Optional**: `phone`, `notes`, `source`, `medium`, `campaign`\n\n### Endpoints\n- **Test**: `/webhook-test/lead-webform`\n- **Prod**: `/webhook/lead-webform`\n\n### Quick test\ncurl -X POST \"https://<host>/webhook-test/lead-webform\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-webhook-token: <secret>\" \\\n  -d '{\"firstname\":\"John\",\"lastname\":\"Doe\",\"email\":\"john@ex.com\",\n       \"phone\":\"+39333...\", \"notes\":\"Demo\",\n       \"source\":\"Ads\",\"medium\":\"Website\",\"campaign\":\"Spring 2025\"}'\n"
      },
      "typeVersion": 1
    },
    {
      "id": "45fba007-2cc9-45a9-aafc-d3ba6281cc56",
      "name": "Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -1088,
        544
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "noData"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c69cb8d1-78c8-4196-b3c4-384954e7e230",
      "name": "Required data missing?",
      "type": "n8n-nodes-base.if",
      "position": [
        -3120,
        512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "60ce05fd-ff94-4e59-9ebc-09c058b8f7fb",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.firstname }}",
              "rightValue": ""
            },
            {
              "id": "714cc2e5-e781-4fc2-a6af-af3e16a58d21",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.lastname }}",
              "rightValue": ""
            },
            {
              "id": "c09b99e1-faf0-44c4-a9e1-2d4192a61c9c",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.email }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e89b2169-b944-407b-a3c5-05b1ed6c66e8",
      "name": "Bad Request",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -2944,
        32
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "noData"
      },
      "typeVersion": 1.4
    },
    {
      "id": "b3b29fa8-89c6-42b4-9455-f6ed4110416f",
      "name": "Source ID Validation",
      "type": "n8n-nodes-base.code",
      "position": [
        -2512,
        176
      ],
      "parameters": {
        "jsCode": "const f = items[0]?.json;\nreturn [{ json: { source_id: f?.id ?? null } }]; \n"
      },
      "typeVersion": 2
    },
    {
      "id": "8c3852d9-bccc-41d6-8b44-ff22bbb9b458",
      "name": "Medium ID Validation",
      "type": "n8n-nodes-base.code",
      "position": [
        -2512,
        304
      ],
      "parameters": {
        "jsCode": "const f = items[0]?.json;\nreturn [{ json: { medium_id: f?.id ?? null } }]; \n"
      },
      "typeVersion": 2
    },
    {
      "id": "28199eb1-83e3-45a5-b6e8-cb1bad911bdb",
      "name": "Campaign ID Validation",
      "type": "n8n-nodes-base.code",
      "position": [
        -2512,
        432
      ],
      "parameters": {
        "jsCode": "const f = items[0]?.json;\nreturn [{ json: { campaign_id: f?.id ?? null } }]; \n"
      },
      "typeVersion": 2
    },
    {
      "id": "ca35714a-9459-484f-8b42-f59f26286533",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2736,
        48
      ],
      "parameters": {
        "color": 2,
        "width": 368,
        "height": 544,
        "content": "## Get Marketing Data\n"
      },
      "typeVersion": 1
    },
    {
      "id": "cbe239c6-de01-4d18-b523-bb856bf405bb",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4016,
        240
      ],
      "parameters": {
        "color": 6,
        "width": 608,
        "height": 800,
        "content": "## How it works\n\n1) **Ingest**\n   - Webhook receives POST at the path above.\n   - Header Auth is required (`x-webhook-token`).\n\n2) **Validate**\n   - IF node checks required fields; if any is missing \u2192 Respond **400 Bad Request**.\n\n3) **UTM lookup**\n   - Three Odoo `getAll` nodes fetch IDs by name:\n     - `utm.source` \u2192 `source_id`\n     - `utm.medium` \u2192 `medium_id`\n     - `utm.campaign` \u2192 `campaign_id`\n   - If not found, the ID remains `null` (safe default).\n\n4) **Consolidate**\n   - **Merge (append)** feeds a **Code** node that merges the 4 inputs into one clean object:\n     `{ name, contact_name, email_from, phone, description, type: \"lead\",\n        campaign_id, source_id, medium_id }`\n\n5) **Create in Odoo**\n   - Odoo node (`crm.lead \u2192 create`) writes the lead using standard fields.\n\n6) **Respond**\n   - **Success 200** \u2192 `{ \"status\": \"ok\", \"lead_id\": <id> }`\n   - **Bad Request 400** with a clear error message when validation fails.\n\n**Notes**\n- Recent Odoo versions don\u2019t have `mobile`: use **`phone`**.\n- Keep credentials out of the template; users will select theirs after import.\n- If you later want to auto-create missing UTM records, add an IF after each `getAll` and a `create` on `utm.*`.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "9f880e8b-56c7-42f8-a18d-e3b9ae3cfb00",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Prepare Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Lead": {
      "main": [
        [
          {
            "node": "Execution Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execution Data": {
      "main": [
        [
          {
            "node": "Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Request": {
      "main": [
        [
          {
            "node": "Create Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTM: Get Medium ID": {
      "main": [
        [
          {
            "node": "Medium ID Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTM: Get Source ID": {
      "main": [
        [
          {
            "node": "Source ID Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Medium ID Validation": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Source ID Validation": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UTM: Get Campaign ID": {
      "main": [
        [
          {
            "node": "Campaign ID Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Campaign ID Validation": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Required data missing?": {
      "main": [
        [
          {
            "node": "Bad Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "UTM: Get Campaign ID",
            "type": "main",
            "index": 0
          },
          {
            "node": "UTM: Get Medium ID",
            "type": "main",
            "index": 0
          },
          {
            "node": "UTM: Get Source ID",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Webhook - Lead Webform": {
      "main": [
        [
          {
            "node": "Required data missing?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Create records in Odoo from any webform via a secure webhook. The workflow validates required fields, resolves UTMs by name (source, medium, campaign) and writes standard lead fields in Odoo. Clean, portable, and production-ready. ✅ Secure Webhook with Header Auth () ✅ Required…

Source: https://n8n.io/workflows/7289/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

This workflow captures raw lead data from a Webhook and formats it into a clean, structured object — perfectly tailored for Odoo CRM and create lead. It supports Odoo versions 15, 16, 17, and 18, both

Odoo
Marketing & Ads

Ad agencies needing automated lead capture. Sales teams fighting fraud and scoring leads. B2B SaaS companies nurturing prospects. Marketing pros boosting sales pipelines. Captures leads via Webhook fr

HTTP Request, Google Sheets, Slack +2
Marketing & Ads

Store leads in a SQL Server database via REST API with automatic scoring and Slack notifications.

HTTP Request, Slack, Error Trigger
Marketing & Ads

For specialized B2B agencies, consultancies, and MSPs, lead routing isn't about guessing "purchasing temperature" it’s about distinct operational tracks. An "Infrastructure Audit" requires vastly diff

Email Send
Marketing & Ads

Instantly reach new leads on WhatsApp when they submit a form (Typeform, JotForm, Google Forms, or any webhook-enabled form) using MoltFlow (https://molt.waiflow.app). Leads are also logged to Google

HTTP Request, Google Sheets