This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
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 →
{
"updatedAt": "2026-03-30T13:14:26.965Z",
"createdAt": "2026-03-30T13:14:26.965Z",
"id": "HDlrxGe1GI2uGKBV",
"name": "OTP Verification System",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"id": "wh-send",
"name": "Receive OTP Request",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "send-otp",
"responseMode": "responseNode",
"options": {}
}
},
{
"id": "code-gen",
"name": "Validate and Generate OTP",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
300
],
"parameters": {
"jsCode": "const body = $input.first().json.body || $input.first().json;\nconst email = (body.email || '').trim().toLowerCase();\nconst name = (body.name || 'User').trim();\nconst purpose = body.purpose || 'verify';\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nif (!emailRegex.test(email)) { return [{ json: { valid: false, error: 'Invalid email address format', email, name, purpose } }]; }\nconst otp = Math.floor(100000 + Math.random() * 900000).toString();\nconst sessionId = 'OTP_' + Date.now() + '_' + Math.random().toString(36).substring(2, 8).toUpperCase();\nconst now = new Date();\nconst expiryTime = new Date(now.getTime() + 10 * 60 * 1000);\nconst purposeText = { login: 'log in to your account', signup: 'complete your registration', reset_password: 'reset your password', verify: 'verify your identity' }[purpose] || 'verify your identity';\nreturn [{ json: { valid: true, email, name, purpose, purpose_text: purposeText, otp, session_id: sessionId, generated_at: now.toISOString(), expiry_time: expiryTime.toISOString(), expiry_minutes: 10, attempts: 0 } }];"
}
},
{
"id": "if-valid",
"name": "Is Email Valid",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
440,
300
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
},
{
"id": "shape-otp-log",
"name": "Shape OTP Log Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
200
],
"parameters": {
"jsCode": "return [{ json: { 'Timestamp': $json.generated_at, 'Email': $json.email, 'OTP Code': $json.otp, 'OTP Sent At': $json.generated_at, 'Verified At': 'Pending', 'Status': 'Sent', 'Attempts': 0, 'IP Address': '', 'Session ID': $json.session_id, 'Expiry Time': $json.expiry_time, _email: $json.email, _name: $json.name, _otp: $json.otp, _session_id: $json.session_id, _purpose_text: $json.purpose_text, _expiry_minutes: $json.expiry_minutes, _generated_at: $json.generated_at, _expiry_time: $json.expiry_time } }];"
}
},
{
"id": "log-otp",
"name": "Log OTP to Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
880,
200
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1NUwfMYpOXuLI8ysTM0_siFxIL-jgNUGjFAD5AH2U-MY",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "OTP_Log",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
}
},
{
"id": "respond-success",
"name": "Send OTP Success Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1100,
200
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: true, message: 'OTP sent successfully to ' + $json._email, session_id: $json._session_id, expires_in_minutes: 10, hint: 'Check your email inbox and spam folder' }) }}",
"options": {
"responseCode": 200
}
}
},
{
"id": "send-email",
"name": "Send OTP Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1320,
200
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"parameters": {
"sendTo": "={{ $json._email }}",
"subject": "={{ $json._otp + ' is your verification code' }}",
"emailType": "html",
"message": "={{ '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body style=\"margin:0;padding:0;background:#f4f4f4;font-family:Arial,sans-serif\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f4f4f4;padding:40px 20px\"><tr><td align=\"center\"><table width=\"560\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.08)\"><tr><td style=\"background:linear-gradient(135deg,#1a1a2e,#16213e);padding:32px;text-align:center\"><h1 style=\"color:#ffffff;margin:0;font-size:24px;font-weight:700\">Verification Code</h1><p style=\"color:#8892a4;margin:8px 0 0;font-size:14px\">One-time password for ' + $json._purpose_text + '</p></td></tr><tr><td style=\"padding:40px 40px 24px\"><p style=\"color:#333;font-size:16px;margin:0 0 8px\">Hello <strong>' + $json._name + '</strong>,</p><p style=\"color:#555;font-size:15px;line-height:1.6;margin:0 0 32px\">Here is your one-time verification code. This code is valid for <strong>' + $json._expiry_minutes + ' minutes</strong>.</p><div style=\"background:#f8f9ff;border:2px dashed #7c4dff;border-radius:12px;padding:28px;text-align:center;margin:0 0 28px\"><p style=\"color:#888;font-size:12px;text-transform:uppercase;letter-spacing:2px;margin:0 0 12px\">Your OTP Code</p><div style=\"font-size:48px;font-weight:800;letter-spacing:12px;color:#1a1a2e;font-family:monospace\">' + $json._otp + '</div><p style=\"color:#888;font-size:13px;margin:12px 0 0\">Expires in ' + $json._expiry_minutes + ' minutes</p></div><div style=\"background:#fff3e0;border-left:4px solid #ff9800;border-radius:0 8px 8px 0;padding:16px;margin:0 0 28px\"><p style=\"margin:0;color:#e65100;font-size:13px;line-height:1.6\"><strong>Security notice:</strong> Never share this code with anyone.</p></div><p style=\"color:#999;font-size:12px;line-height:1.6;margin:0\">Session ID: ' + $json._session_id + '<br>Valid until: ' + new Date($json._expiry_time).toLocaleString() + '</p></td></tr><tr><td style=\"background:#f8f9fa;padding:20px 40px;text-align:center;border-top:1px solid #e8e8e8\"><p style=\"color:#999;font-size:12px;margin:0\">This is an automated message. Please do not reply.</p></td></tr></table></td></tr></table></body></html>' }}",
"options": {}
}
},
{
"id": "respond-error",
"name": "Send Error Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
660,
420
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: false, error: $json.error, message: 'Please provide a valid email address' }) }}",
"options": {
"responseCode": 400
}
}
},
{
"id": "wh-verify",
"name": "Receive OTP Verification",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
700
],
"parameters": {
"httpMethod": "POST",
"path": "verify-otp",
"responseMode": "responseNode",
"options": {}
}
},
{
"id": "extract-submission",
"name": "Extract Submission",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
700
],
"parameters": {
"jsCode": "const body = $input.first().json.body || $input.first().json;\nreturn [{ json: { submitted_otp: String(body.otp || '').trim(), submitted_email: String(body.email || '').trim().toLowerCase(), session_id: String(body.session_id || '').trim() } }];"
}
},
{
"id": "read-otp",
"name": "Read OTP Record",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
440,
700
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "1NUwfMYpOXuLI8ysTM0_siFxIL-jgNUGjFAD5AH2U-MY",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "OTP_Log",
"mode": "name"
},
"filtersUI": {
"values": [
{
"lookupColumn": "Session ID",
"lookupValue": "={{ $json.session_id }}"
}
]
},
"options": {
"returnFirstMatch": true
}
}
},
{
"id": "if-record-found",
"name": "Record Found",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
660,
700
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c3",
"leftValue": "={{ $json['OTP Code'] !== undefined && $json['OTP Code'] !== '' }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
},
{
"id": "respond-not-found",
"name": "Session Not Found Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
880,
820
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: false, message: 'OTP session not found. Please request a new OTP.', reason: 'SESSION_NOT_FOUND', session_id: $('Extract Submission').item.json.session_id }) }}",
"options": {
"responseCode": 400
}
}
},
{
"id": "code-verify",
"name": "Verify OTP Logic",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
600
],
"parameters": {
"jsCode": "const submission = $('Extract Submission').first().json;\nconst record = $input.first().json;\nconst submittedOtp = submission.submitted_otp;\nconst submittedEmail = submission.submitted_email;\nconst sessionId = submission.session_id;\nconst storedOtp = String(record['OTP Code'] || '').trim();\nconst storedEmail = String(record['Email'] || '').trim().toLowerCase();\nconst expiryTime = new Date(record['Expiry Time']);\nconst now = new Date();\nconst attempts = parseInt(record['Attempts'] || 0) + 1;\nif (record['Status'] === 'Verified') { return [{ json: { verified: false, reason: 'ALREADY_USED', message: 'This OTP has already been used.', email: submittedEmail, session_id: sessionId, attempts } }]; }\nif (now > expiryTime) { return [{ json: { verified: false, reason: 'EXPIRED', message: 'Your OTP has expired. Please request a new one.', email: submittedEmail, session_id: sessionId, attempts, expired_at: expiryTime.toISOString() } }]; }\nif (attempts > 5) { return [{ json: { verified: false, reason: 'TOO_MANY_ATTEMPTS', message: 'Too many failed attempts. Please request a new OTP.', email: submittedEmail, session_id: sessionId, attempts } }]; }\nif (submittedEmail !== storedEmail) { return [{ json: { verified: false, reason: 'EMAIL_MISMATCH', message: 'Email does not match this OTP session.', email: submittedEmail, session_id: sessionId, attempts } }]; }\nif (submittedOtp !== storedOtp) { return [{ json: { verified: false, reason: 'WRONG_OTP', message: 'Incorrect OTP. Please try again.', email: submittedEmail, session_id: sessionId, attempts, remaining_attempts: Math.max(0, 5 - attempts) } }]; }\nreturn [{ json: { verified: true, reason: 'SUCCESS', message: 'OTP verified successfully.', email: submittedEmail, session_id: sessionId, verified_at: now.toISOString(), attempts, token: 'VERIFIED_' + sessionId + '_' + Date.now() } }];"
}
},
{
"id": "if-verified",
"name": "Is OTP Valid",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1100,
600
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c2",
"leftValue": "={{ $json.verified }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
}
}
},
{
"id": "shape-success",
"name": "Shape Success Update",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
500
],
"parameters": {
"jsCode": "return [{ json: { 'Session ID': $json.session_id, 'Status': 'Verified', 'Verified At': $json.verified_at || '', 'Attempts': $json.attempts, _verified: true, _message: $json.message, _reason: $json.reason, _session_id: $json.session_id, _token: $json.token, _verified_at: $json.verified_at, _email: $json.email } }];"
}
},
{
"id": "update-success",
"name": "Update OTP Status in Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1540,
500
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1NUwfMYpOXuLI8ysTM0_siFxIL-jgNUGjFAD5AH2U-MY",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "OTP_Log",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [
"Session ID"
],
"schema": []
},
"options": {}
}
},
{
"id": "respond-verified",
"name": "Verification Result Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1760,
500
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: true, message: $json._message, reason: $json._reason, session_id: $json._session_id, token: $json._token || null, verified_at: $json._verified_at || null }) }}",
"options": {
"responseCode": 200
}
}
},
{
"id": "send-success-email",
"name": "Send Success Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1980,
500
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"parameters": {
"sendTo": "={{ $json._email }}",
"subject": "\u2705 Identity Verified Successfully",
"emailType": "html",
"message": "={{ '<div style=\"font-family:Arial,sans-serif;max-width:560px;margin:0 auto;background:#fff;border-radius:16px;overflow:hidden\"><div style=\"background:linear-gradient(135deg,#4CAF50,#2e7d32);padding:32px;text-align:center\"><div style=\"font-size:48px\">\u2705</div><h1 style=\"color:#fff;margin:0;font-size:24px\">Verified Successfully!</h1></div><div style=\"padding:32px\"><p style=\"color:#333;font-size:16px\">Your identity has been verified successfully.</p><div style=\"background:#e8f5e9;border-radius:8px;padding:16px;margin:16px 0\"><p style=\"margin:0;color:#2e7d32;font-size:14px\">\u2705 Email: ' + $json._email + '</p><p style=\"margin:8px 0 0;color:#2e7d32;font-size:14px\">\u2705 Verified at: ' + new Date($json._verified_at).toLocaleString() + '</p></div></div></div>' }}",
"options": {}
}
},
{
"id": "shape-fail",
"name": "Shape Fail Update",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
720
],
"parameters": {
"jsCode": "return [{ json: { 'Session ID': $json.session_id, 'Status': $json.reason, 'Attempts': $json.attempts, _message: $json.message, _reason: $json.reason, _session_id: $json.session_id, _remaining: $json.remaining_attempts, _email: $json.email } }];"
}
},
{
"id": "update-fail",
"name": "Update OTP Fail Status",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1540,
720
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1NUwfMYpOXuLI8ysTM0_siFxIL-jgNUGjFAD5AH2U-MY",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "OTP_Log",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [
"Session ID"
],
"schema": []
},
"options": {}
}
},
{
"id": "respond-fail",
"name": "Verification Fail Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1760,
720
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: false, message: $json._message, reason: $json._reason, session_id: $json._session_id, remaining_attempts: $json._remaining || null }) }}",
"options": {
"responseCode": 400
}
}
},
{
"id": "send-fail-email",
"name": "Send Failure Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1980,
720
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"parameters": {
"sendTo": "={{ $json._email }}",
"subject": "={{ '\u274c OTP Verification Failed \u2014 ' + $json._reason }}",
"emailType": "html",
"message": "={{ '<div style=\"font-family:Arial,sans-serif;max-width:560px;margin:0 auto;background:#fff;border-radius:16px;overflow:hidden\"><div style=\"background:linear-gradient(135deg,#f44336,#b71c1c);padding:32px;text-align:center\"><div style=\"font-size:48px\">\u274c</div><h1 style=\"color:#fff;margin:8px 0 0;font-size:22px\">Verification Failed</h1></div><div style=\"padding:32px\"><p style=\"color:#333;font-size:16px\">' + $json._message + '</p>' + ($json._remaining > 0 ? '<p style=\"color:#ff9800;font-size:14px\">\u26a0\ufe0f Attempts remaining: ' + $json._remaining + '</p>' : '<p style=\"color:#f44336;font-size:14px\">\ud83d\udd12 Please request a new OTP.</p>') + '<p style=\"color:#999;font-size:13px\">Reason code: ' + $json._reason + '</p></div></div>' }}",
"options": {}
}
}
],
"connections": {
"Receive OTP Request": {
"main": [
[
{
"node": "Validate and Generate OTP",
"type": "main",
"index": 0
}
]
]
},
"Validate and Generate OTP": {
"main": [
[
{
"node": "Is Email Valid",
"type": "main",
"index": 0
}
]
]
},
"Is Email Valid": {
"main": [
[
{
"node": "Shape OTP Log Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Error Response",
"type": "main",
"index": 0
}
]
]
},
"Shape OTP Log Data": {
"main": [
[
{
"node": "Log OTP to Sheets",
"type": "main",
"index": 0
}
]
]
},
"Log OTP to Sheets": {
"main": [
[
{
"node": "Send OTP Success Response",
"type": "main",
"index": 0
}
]
]
},
"Send OTP Success Response": {
"main": [
[
{
"node": "Send OTP Email",
"type": "main",
"index": 0
}
]
]
},
"Receive OTP Verification": {
"main": [
[
{
"node": "Extract Submission",
"type": "main",
"index": 0
}
]
]
},
"Extract Submission": {
"main": [
[
{
"node": "Read OTP Record",
"type": "main",
"index": 0
}
]
]
},
"Read OTP Record": {
"main": [
[
{
"node": "Record Found",
"type": "main",
"index": 0
}
]
]
},
"Record Found": {
"main": [
[
{
"node": "Verify OTP Logic",
"type": "main",
"index": 0
}
],
[
{
"node": "Session Not Found Response",
"type": "main",
"index": 0
}
]
]
},
"Verify OTP Logic": {
"main": [
[
{
"node": "Is OTP Valid",
"type": "main",
"index": 0
}
]
]
},
"Is OTP Valid": {
"main": [
[
{
"node": "Shape Success Update",
"type": "main",
"index": 0
}
],
[
{
"node": "Shape Fail Update",
"type": "main",
"index": 0
}
]
]
},
"Shape Success Update": {
"main": [
[
{
"node": "Update OTP Status in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Update OTP Status in Sheets": {
"main": [
[
{
"node": "Verification Result Response",
"type": "main",
"index": 0
}
]
]
},
"Verification Result Response": {
"main": [
[
{
"node": "Send Success Email",
"type": "main",
"index": 0
}
]
]
},
"Shape Fail Update": {
"main": [
[
{
"node": "Update OTP Fail Status",
"type": "main",
"index": 0
}
]
]
},
"Update OTP Fail Status": {
"main": [
[
{
"node": "Verification Fail Response",
"type": "main",
"index": 0
}
]
]
},
"Verification Fail Response": {
"main": [
[
{
"node": "Send Failure Email",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false
},
"staticData": null,
"meta": null,
"versionId": "388006b6-f459-4683-b4dc-f9a0b79bc4c8",
"activeVersionId": "388006b6-f459-4683-b4dc-f9a0b79bc4c8",
"versionCounter": 16,
"triggerCount": 2,
"tags": [],
"shared": [
{
"updatedAt": "2026-03-30T13:14:26.968Z",
"createdAt": "2026-03-30T13:14:26.968Z",
"role": "workflow:owner",
"workflowId": "HDlrxGe1GI2uGKBV",
"projectId": "KaaB9eLEU6T6FdOr",
"project": {
"updatedAt": "2026-03-29T11:22:07.297Z",
"createdAt": "2026-03-15T10:25:50.376Z",
"id": "KaaB9eLEU6T6FdOr",
"name": "Admin User <admin@n8n.local>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "28a0614f-963b-453e-b373-b2cbab3d7b07"
}
}
]
}
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.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
OTP Verification System. Uses googleSheets, gmail. Webhook trigger; 23 nodes.
Source: https://github.com/Kavix28/Kapoor-Associates/blob/0862f11e9db134ae2511bb3f59b4dbf0417dfe81/n8n-workflows/otp-workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Transform your visitor management process with this fully automated, enterprise-grade workflow. The Verified Visitor Pass Generator eliminates manual data entry, prevents fake registrations through em
Automated email verification and welcome email workflow that validates new user signups, prevents fake emails, and creates a seamless onboarding experience with real-time team notifications.
Transform your referral program into a fully automated, fraud-resistant system that delivers professional rewards to verified referrers. This workflow combines email validation, dynamic coupon generat
This comprehensive workflow automates the entire event RSVP process from form submission to attendee confirmation, including real-time email validation and personalized digital badge generation.
This workflow automatically generates beautiful, personalized promotional cards with QR codes and sends them via email to verified users. Perfect for e-commerce stores, marketing campaigns, and custom