AutomationFlowsGeneral › Filter Spam From Webhook Form Submissions Using Honeypot and Timing Checks

Filter Spam From Webhook Form Submissions Using Honeypot and Timing Checks

ByFlorian Eiche @jagged on n8n.io

Website owners, agencies, or developers who receive contact form submissions via webhook and want to block bots and spam without CAPTCHAs, keeping the user experience clean and friction-free.

Webhook trigger★★★★☆ complexity12 nodes
General Trigger: Webhook Nodes: 12 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #14028 — 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": "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
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

Website owners, agencies, or developers who receive contact form submissions via webhook and want to block bots and spam without CAPTCHAs, keeping the user experience clean and friction-free.

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

More General workflows → · Browse all categories →

Related workflows

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

General

A clean, extensible REST-style API routing template for n8n webhooks with up to 3 path levels. Serves API routes via Webhooks with path variables Normalizes incoming requests into "global" REQUEST and

General

PUQ Docker NextCloud deploy. Uses respondToWebhook, stickyNote, httpRequest, ssh. Webhook trigger; 44 nodes.

HTTP Request, Ssh
General

puq-docker-immich-deploy. Uses respondToWebhook, ssh, stickyNote. Webhook trigger; 35 nodes.

Ssh
General

Analyze_email_headers_for_IPs_and_spoofing__3. Uses stickyNote, respondToWebhook, itemLists, httpRequest. Webhook trigger; 35 nodes.

Item Lists, HTTP Request
General

puq-docker-n8n-deploy. Uses respondToWebhook, ssh, stickyNote. Webhook trigger; 34 nodes.

Ssh