{
  "id": "VvAhNqQ33ba3Ufgs",
  "name": "Handle website contact form submissions with validation, Google Sheets and email auto-reply",
  "tags": [],
  "nodes": [
    {
      "id": "note-overview",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1456,
        -640
      ],
      "parameters": {
        "width": 520,
        "height": 1012,
        "content": "## Handle website contact form submissions with validation, Google Sheets and email auto-reply\n\n### Who's it for\n\nAnyone with a portfolio, agency, or small-business website who wants a reliable contact form backend without building a server \u2014 submissions are validated, logged, and answered automatically.\n\n### How it works\n\n1. A POST webhook receives the contact form submission from your website.\n2. Every input is validated \u2014 name (\u2264 100 chars), email (regex format check), and message (\u2264 2 000 chars) are required.\n3. Valid submissions are appended to Google Sheets and trigger a personalised confirmation email in parallel.\n4. The caller receives a JSON success (200) or error (400) response, with CORS headers for browser calls.\n\n### How to set up\n\n- [ ] Open **Workflow Configuration** and set your sender email + website origin\n- [ ] Connect your Google Sheets credential and select your spreadsheet\n- [ ] Add columns to the sheet: Name, Email, Subject, Message, Phone, Timestamp, Source\n- [ ] Connect your SMTP credential\n- [ ] Point your website form at the webhook URL\n\n### Requirements\n\n- Google Sheets credential (OAuth2)\n- SMTP credential\n\n### How to customize\n\nEdit the confirmation email HTML to match your brand, adjust field length limits in Guard Input, add extra form fields, or swap Google Sheets for a database node."
      },
      "typeVersion": 1
    },
    {
      "id": "note-webhook",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -912,
        -672
      ],
      "parameters": {
        "color": 7,
        "height": 568,
        "content": "## Webhook Trigger\n\nReceives POST requests from your website's contact form.\n\nExpected JSON body:\n`{ \"name\", \"email\", \"message\", \"subject\" (optional), \"phone\" (optional) }`\n\nRestrict **Allowed Origins** to your website domain before going live."
      },
      "typeVersion": 1
    },
    {
      "id": "note-config",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -656,
        -560
      ],
      "parameters": {
        "color": 7,
        "height": 568,
        "content": "## Configure\n\nSet the two values every user must change:\n\n- **senderEmail** \u2014 the From address for the confirmation email\n- **allowedOrigin** \u2014 your website URL, used in the CORS response headers"
      },
      "typeVersion": 1
    },
    {
      "id": "note-validate",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        -560
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 640,
        "content": "## Validate & Prepare Data\n\nGuards all required fields before any downstream work:\n- **name** \u2014 non-empty, \u2264 100 chars\n- **email** \u2014 regex format check\n- **message** \u2014 non-empty, \u2264 2 000 chars\n\nInvalid submissions receive an immediate 400 error response. Valid data is normalised (trimmed, email lowercased) and assembled for the next steps."
      },
      "typeVersion": 1
    },
    {
      "id": "note-store",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        112,
        -560
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 640,
        "content": "## Store & Notify\n\nRuns after validation:\n- **Append to Contact Sheet** \u2014 logs every submission to Google Sheets (append-only; no row overwrites)\n- **Send Confirmation Email** \u2014 sends a personalised HTML thank-you email to the visitor via SMTP. Customise the HTML to match your brand."
      },
      "typeVersion": 1
    },
    {
      "id": "note-respond",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        -560
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 440,
        "content": "## Respond\n\nSends a JSON response back to the contact form:\n- **200** on success\n- **400** on invalid input\n\nCORS headers use the **allowedOrigin** from Workflow Configuration."
      },
      "typeVersion": 1
    },
    {
      "id": "node-webhook",
      "name": "When Contact Form Submitted",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        -864,
        -272
      ],
      "parameters": {
        "path": "contact-form",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "node-config",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -592,
        -272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "sender-email",
              "name": "senderEmail",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "allowed-origin",
              "name": "allowedOrigin",
              "type": "string",
              "value": "https://yourdomain.com"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "node-guard",
      "name": "Guard Input",
      "type": "n8n-nodes-base.if",
      "position": [
        -368,
        -272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "name-not-empty",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ ($json.body.name || '').trim().length }}",
              "rightValue": 0
            },
            {
              "id": "name-max-length",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ ($json.body.name || '').length }}",
              "rightValue": 100
            },
            {
              "id": "email-format",
              "operator": {
                "type": "string",
                "operation": "regex"
              },
              "leftValue": "={{ $json.body.email || '' }}",
              "rightValue": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
            },
            {
              "id": "message-not-empty",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ ($json.body.message || '').trim().length }}",
              "rightValue": 0
            },
            {
              "id": "message-max-length",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ ($json.body.message || '').length }}",
              "rightValue": 2000
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "node-setdata",
      "name": "Set Form Data",
      "type": "n8n-nodes-base.set",
      "position": [
        -128,
        -272
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "name",
              "name": "name",
              "type": "string",
              "value": "={{ $json.body.name.trim() }}"
            },
            {
              "id": "email",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.email.trim().toLowerCase() }}"
            },
            {
              "id": "subject",
              "name": "subject",
              "type": "string",
              "value": "={{ ($json.body.subject || 'Contact Form Submission').trim() }}"
            },
            {
              "id": "message",
              "name": "message",
              "type": "string",
              "value": "={{ $json.body.message.trim() }}"
            },
            {
              "id": "phone",
              "name": "phone",
              "type": "string",
              "value": "={{ ($json.body.phone || 'Not provided').trim() }}"
            },
            {
              "id": "timestamp",
              "name": "timestamp",
              "type": "string",
              "value": "={{ new Date().toISOString() }}"
            },
            {
              "id": "source",
              "name": "source",
              "type": "string",
              "value": "Website Contact Form"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "node-email",
      "name": "Send Confirmation Email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        144,
        -304
      ],
      "parameters": {
        "html": "=<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background-color:#f0f4f8;font-family:'Segoe UI',Arial,sans-serif;\">\n<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:#f0f4f8;padding:32px 16px;\">\n<tr><td align=\"center\">\n<table role=\"presentation\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;\">\n  <tr>\n    <td style=\"background:linear-gradient(135deg,#0f4c81 0%,#1a73c8 100%);padding:28px 36px;\">\n      <p style=\"margin:0;font-size:22px;font-weight:700;color:#ffffff;\">\u2705 Message received!</p>\n    </td>\n  </tr>\n  <tr>\n    <td style=\"padding:28px 36px;color:#374151;font-size:15px;line-height:1.7;\">\n      <p style=\"margin:0 0 16px;\">Hi <strong>{{ $json.name.split(' ')[0] }}</strong>,</p>\n      <p style=\"margin:0 0 16px;\">Thanks for getting in touch \u2014 your message has been received and I'll get back to you within <strong>24\u201348 hours</strong>.</p>\n      <table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"margin:0 0 16px;\">\n        <tr>\n          <td style=\"background:#f8fafc;border-left:3px solid #1a73c8;border-radius:6px;padding:16px 20px;\">\n            <p style=\"margin:0 0 6px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#1a73c8;\">Your message</p>\n            <p style=\"margin:0 0 4px;font-size:13px;color:#6b7280;\"><strong>Subject:</strong> {{ $json.subject }}</p>\n            <p style=\"margin:0;font-size:13px;color:#374151;\">{{ $json.message }}</p>\n          </td>\n        </tr>\n      </table>\n      <p style=\"margin:0;\">Talk soon,<br><strong>[Your Name]</strong></p>\n    </td>\n  </tr>\n  <tr>\n    <td style=\"background:#f8fafc;border-top:1px solid #e2e8f0;padding:14px 36px;text-align:center;font-size:11px;color:#9ca3af;\">\n      You received this email because you contacted us via our website.\n    </td>\n  </tr>\n</table>\n</td></tr>\n</table>\n</body>\n</html>",
        "options": {},
        "subject": "=Thanks for reaching out, {{ $json.name }}!",
        "toEmail": "={{ $json.email }}",
        "fromEmail": "={{ $('Workflow Configuration').item.json.senderEmail }}"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "node-sheets",
      "name": "Append to Contact Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        144,
        -112
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4,
      "alwaysOutputData": true
    },
    {
      "id": "node-error",
      "name": "Send Error Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -128,
        -80
      ],
      "parameters": {
        "options": {
          "responseCode": 400,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "={{ $('Workflow Configuration').item.json.allowedOrigin }}"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={\n  \"status\": \"error\",\n  \"message\": \"Please provide a valid name, email address, and message (max 2000 characters).\",\n  \"timestamp\": \"{{ new Date().toISOString() }}\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "node-success",
      "name": "Send Success Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        448,
        -304
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "={{ $('Workflow Configuration').item.json.allowedOrigin }}"
              },
              {
                "name": "Access-Control-Allow-Methods",
                "value": "POST, OPTIONS"
              },
              {
                "name": "Access-Control-Allow-Headers",
                "value": "Content-Type"
              },
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={\n  \"status\": \"success\",\n  \"message\": \"Thank you for your message! We've received it and will get back to you soon.\",\n  \"timestamp\": \"{{ $('Set Form Data').item.json.timestamp }}\",\n  \"data\": {\n    \"name\": \"{{ $('Set Form Data').item.json.name }}\",\n    \"email\": \"{{ $('Set Form Data').item.json.email }}\",\n    \"subject\": \"{{ $('Set Form Data').item.json.subject }}\"\n  }\n}"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "909d0e2b-4b75-4678-bca7-0799b04ca37f",
  "nodeGroups": [],
  "connections": {
    "Guard Input": {
      "main": [
        [
          {
            "node": "Set Form Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Form Data": {
      "main": [
        [
          {
            "node": "Append to Contact Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Configuration": {
      "main": [
        [
          {
            "node": "Guard Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Contact Sheet": {
      "main": [
        [
          {
            "node": "Send Confirmation Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Confirmation Email": {
      "main": [
        [
          {
            "node": "Send Success Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Contact Form Submitted": {
      "main": [
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}