{
  "id": "OWdZk9zJyRYut0Um",
  "name": "Filter spam from webhook form submissions with honeypot and bot detection",
  "tags": [],
  "nodes": [
    {
      "id": "4c475285-00b7-41e5-8877-edd7036ecdfd",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -608,
        1200
      ],
      "parameters": {
        "color": 4,
        "width": 800,
        "height": 736,
        "content": "## Filter Spam from Webhook Form Submissions\n\nThis workflow acts as a **spam filter backend** for your website contact forms. It receives form submissions via webhook, runs three automated checks, and either silently blocks spam or forwards legitimate submissions for processing.\n\n### Who is this for?\nWebsite owners, agencies, or developers who receive form submissions and want to block bots and spam **without CAPTCHAs**, keeping the user experience clean.\n\n### How it works\n1. Your frontend sends a POST request with form data, a hidden honeypot field, and a client-side timestamp\n2. The workflow runs three spam checks:\n   - **Honeypot detection**: If the hidden field contains data, it's a bot\n   - **Timing analysis**: If the form was submitted in under 2 seconds, it's a bot\n   - **Disposable email detection**: Checks the email domain against a configurable blocklist\n3. **Spam**: Returns a silent 200 OK (the bot thinks it worked, but nothing is forwarded)\n4. **Legitimate**: Returns success with the cleaned form data for downstream processing\n\n### Setup\n1. Activate the workflow\n2. Add the honeypot field and timestamp to your frontend form (see Step 1 sticky note)\n3. Optionally adjust the spam rules in the **Configure Spam Rules** node\n\n### How to customize\n- Add your own disposable email domains to the blocklist\n- Adjust the minimum submission time threshold\n- Connect email, Slack, or CRM nodes after the **Legit** branch to forward real submissions\n\n**Author:** Florian Eiche, [eiche-digital.de](https://eiche-digital.de)"
      },
      "typeVersion": 1
    },
    {
      "id": "2acb5cb6-da99-404d-beea-ef7f4b41c695",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        1200
      ],
      "parameters": {
        "width": 436,
        "height": 544,
        "content": "### Step 1: Receive Form Data\nPOST request with JSON body:\n```json\n{\n  \"name\": \"Max Mustermann\",\n  \"email\": \"max@example.com\",\n  \"message\": \"Hello!\",\n  \"website_url\": \"\",\n  \"_timestamp\": \"2026-03-13T10:00:00Z\"\n}\n```\n\n**Frontend HTML example:**\n```html\n<!-- Honeypot (hidden from users, bots fill it) -->\n<div style=\"position:absolute;left:-9999px;\"\n     aria-hidden=\"true\">\n  <input type=\"text\" name=\"website_url\"\n         tabindex=\"-1\" autocomplete=\"off\">\n</div>\n\n<!-- Timestamp (set on page load) -->\n<input type=\"hidden\" name=\"_timestamp\" id=\"ts\">\n<script>\n  document.getElementById('ts').value\n    = new Date().toISOString();\n</script>\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "21c713a0-fcfd-4f7f-ab24-cb6a9b1d1c46",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        1408
      ],
      "parameters": {
        "width": 300,
        "height": 336,
        "content": "### Step 2: Configure Spam Rules\nEdit the **Configure Spam Rules** node to match your form:\n- `honeypotFieldName`: name of the hidden honeypot field\n- `timestampFieldName`: name of the hidden timestamp field\n- `emailFieldName`: name of the email field\n- `minSubmissionTimeSeconds`: minimum seconds to fill the form (default: 2)\n- `disposableDomains`: array of blocked email domains"
      },
      "typeVersion": 1
    },
    {
      "id": "7675c649-41ef-4d76-bfb9-ae99dbeee604",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        1408
      ],
      "parameters": {
        "width": 300,
        "height": 336,
        "content": "### Step 3: Detect Spam\nThe Code node runs three checks:\n1. **Honeypot**: Is the hidden field filled? \u2192 Bot\n2. **Timing**: Was the form submitted in < 2 seconds? \u2192 Bot\n3. **Disposable email**: Is the email domain on the blocklist? \u2192 Spam\n\nOutput: `{ isSpam, reasons[], formData }`, reasons array explains why it was flagged."
      },
      "typeVersion": 1
    },
    {
      "id": "4f20e385-5e53-4839-a1dd-f0e366e97d67",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1296,
        1408
      ],
      "parameters": {
        "width": 276,
        "height": 336,
        "content": "### Step 4: Route Result\n**Spam branch** (top): Returns `200 OK` with a generic thank-you message. The bot thinks the form worked, but nothing is forwarded. This prevents bots from retrying.\n\n**Legit branch** (bottom): Returns `200 OK` with success status and the cleaned form data.\n\n**Add your own nodes** after the IF on the legit branch to forward submissions to email, Slack, a CRM, or a database."
      },
      "typeVersion": 1
    },
    {
      "id": "1b7ee11a-ec19-41d4-a8b4-d28504af34ff",
      "name": "Receive Form Submission",
      "type": "n8n-nodes-base.webhook",
      "position": [
        400,
        1760
      ],
      "parameters": {
        "path": "form-submit",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "255cb5b9-9be7-4eb9-a1b7-2b13b062dabb",
      "name": "Configure Spam Rules",
      "type": "n8n-nodes-base.set",
      "position": [
        736,
        1760
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "{\n  \"honeypotFieldName\": \"website_url\",\n  \"timestampFieldName\": \"_timestamp\",\n  \"emailFieldName\": \"email\",\n  \"minSubmissionTimeSeconds\": 2,\n  \"disposableDomains\": [\n    \"mailinator.com\",\n    \"guerrillamail.com\",\n    \"tempmail.com\",\n    \"throwaway.email\",\n    \"yopmail.com\",\n    \"sharklasers.com\",\n    \"guerrillamailblock.com\",\n    \"grr.la\",\n    \"discard.email\",\n    \"trashmail.com\",\n    \"10minutemail.com\",\n    \"temp-mail.org\",\n    \"fakeinbox.com\",\n    \"mailnesia.com\",\n    \"maildrop.cc\",\n    \"dispostable.com\",\n    \"getairmail.com\",\n    \"mohmal.com\",\n    \"crazymailing.com\"\n  ]\n}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "f25639b5-66d6-4fc8-9771-23396e0d657c",
      "name": "Detect Spam",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        1760
      ],
      "parameters": {
        "jsCode": "// Get form data and spam rules\nconst input = $('Receive Form Submission').first().json.body;\nconst config = $('Configure Spam Rules').first().json;\n\nif (!input || typeof input !== 'object') {\n  return [{\n    json: {\n      isSpam: false,\n      reasons: ['No form data received'],\n      formData: {}\n    }\n  }];\n}\n\nconst honeypotField = config.honeypotFieldName;\nconst timestampField = config.timestampFieldName;\nconst emailField = config.emailFieldName;\nconst minTime = config.minSubmissionTimeSeconds;\nconst disposableDomains = config.disposableDomains || [];\n\nconst reasons = [];\nlet isSpam = false;\n\n// --- Check 1: Honeypot field ---\nif (input[honeypotField] !== undefined && String(input[honeypotField]).trim() !== '') {\n  isSpam = true;\n  reasons.push('Honeypot field was filled \u2014 likely a bot');\n}\n\n// --- Check 2: Submission timing ---\nif (input[timestampField]) {\n  const submittedAt = new Date(input[timestampField]);\n  const now = new Date();\n  const diffSeconds = (now.getTime() - submittedAt.getTime()) / 1000;\n\n  if (diffSeconds >= 0 && diffSeconds < minTime) {\n    isSpam = true;\n    reasons.push(\n      `Form submitted too fast (${diffSeconds.toFixed(1)}s < ${minTime}s threshold)`\n    );\n  }\n}\n\n// --- Check 3: Disposable email domain ---\nif (input[emailField]) {\n  const emailValue = String(input[emailField]).trim();\n  const domain = emailValue.split('@')[1];\n\n  if (domain) {\n    const domainLower = domain.toLowerCase();\n    if (disposableDomains.includes(domainLower)) {\n      isSpam = true;\n      reasons.push(`Disposable email domain detected: ${domainLower}`);\n    }\n  }\n}\n\n// --- Build clean form data (remove honeypot + timestamp fields) ---\nconst formData = {};\nfor (const [key, value] of Object.entries(input)) {\n  if (key !== honeypotField && key !== timestampField) {\n    formData[key] = value;\n  }\n}\n\nreturn [{\n  json: {\n    isSpam,\n    reasons,\n    formData\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "34dd9237-5fe1-4822-ac72-024e9a8f47b8",
      "name": "Is Spam?",
      "type": "n8n-nodes-base.if",
      "position": [
        1392,
        1760
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "condition-spam",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isSpam }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "f6806a95-9a26-40df-acfa-76bbc8031c7f",
      "name": "Silent OK (Spam Blocked)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1712,
        1616
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"message\": \"Thank you for your submission.\"\n}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "4148a064-fbb2-412a-bed1-e5d668dd931f",
      "name": "Forward & Respond (Legit)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1712,
        1904
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, message: 'Submission received.', data: $json.formData }) }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "f3cbeef8-a7b1-47d8-904c-cfcc47f43f44",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        1136
      ],
      "parameters": {
        "color": 6,
        "width": 384,
        "height": 256,
        "content": "## **Why this works:**\n- `position:absolute; left:-9999px` hides the field visually but bots still see it in the DOM\n- `aria-hidden` keeps screen readers from reading it\n- `tabindex=\"-1\"` prevents keyboard navigation into it\n- Do NOT use `display:none`, some bots detect and skip those\n- Field names are configurable in Step 2"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "3e95efb3-b8ab-46b4-ab7e-1a181cc61d10",
  "connections": {
    "Is Spam?": {
      "main": [
        [
          {
            "node": "Silent OK (Spam Blocked)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Forward & Respond (Legit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Spam": {
      "main": [
        [
          {
            "node": "Is Spam?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Spam Rules": {
      "main": [
        [
          {
            "node": "Detect Spam",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Form Submission": {
      "main": [
        [
          {
            "node": "Configure Spam Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}