Масова перевірка e-mail в n8n з Google Sheets
- Для кого
- Користувачі n8n, які ведуть списки лідів у Google Sheets
- Проблема
- Викликати API перевірки окремо для кожного рядка повільно, і так швидко впираєшся в ліміти запитів, а масовому API потрібні завдання, цикл опитування та надійний спосіб зіставити результати з потрібними рядками.
- Рішення
- Один workflow бере до 5 000 рядків, які ще не мають результату, надсилає їхні унікальні адреси одним масовим завданням Cold Leads, опитує завдання кожні 5 секунд, отримує результати, спрямовує кожен рядок за статусом і записує в таблицю статус, оцінку, причину та наступний крок. Запустіть його знову для наступних 5 000.
- Що ви отримаєте
- Чотири заповнені колонки в кожному рядку (verify_status, verify_score, verify_reason, next_step) і workflow, який можна запускати знову, доки не буде оброблено всю таблицю.
Адреси, ідентифікатори та результати в прикладах ілюстративні. Домен example.com зарезервовано для документації, тож реальна перевірка цих адрес поверне invalid.
Приклади коду однакові для всіх мов, коментарі в них — англійською.
Що робить workflow
- Read leads (Google Sheets): усі рядки аркуша, кожен зі своїм row_number.
- Pick next batch (Code): рядки без verify_status; адреси очищено від зайвих пробілів і переведено в нижній регістр, дублікати й клітинки, що не є e-mail-адресами, прибрано; не більше 5 000. Якщо нічого не лишилося, workflow зупиняється тут.
- Create job (HTTP Request): POST /api/v1/verify/bulk з адресами. Відповідь — 202 з id завдання.
- Wait 5 s, потім Get job status (HTTP Request): GET /api/v1/verify/jobs/{id}. Finished? (If) повертає потік до очікування, доки статус не стане done або failed.
- Get results (HTTP Request): той самий запит із results=1, який додає по одному запису на адресу: email, status, score і reason.
- Match rows (Code): кожен рядок аркуша, чия адреса є в результатах, включно з дублікатами, з його row_number.
- Route by status (Switch): valid, risky та invalid ідуть кожен у своє оновлення Google Sheets, яке записує три колонки результату й next_step (send, review або do not send).
Підготуйте таблицю й облікові дані
- Аркушу потрібен рядок заголовків із колонками email, verify_status, verify_score, verify_reason і next_step (інші колонки можуть лишитися).
- Облікові дані (credential) Google Sheets в n8n з доступом до таблиці.
- Облікові дані Header Auth для Cold Leads: Name — x-api-key, Value — ваш секретний ключ (sk_…) з Налаштування → API-ключі. API входить у тариф Business.
Workflow
Скопіюйте JSON і вставте його на порожнє полотно n8n або збережіть як файл та імпортуйте. Потім відкрийте чотири вузли Google Sheets, замініть URL таблиці й назву аркуша (Leads) на свої та виберіть свої облікові дані Google, а в трьох вузлах HTTP Request виберіть облікові дані Header Auth для Cold Leads.
{
"name": "Cold Leads: bulk-verify a Google Sheet",
"nodes": [
{
"parameters": {},
"id": "2fbec667-d7ba-567e-90cc-f8f765280c69",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
300
]
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit",
"mode": "url"
},
"sheetName": {
"__rl": true,
"value": "Leads",
"mode": "name"
},
"options": {}
},
"id": "fee88922-46ce-5cc3-adeb-fdea47dee333",
"name": "Read leads",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
220,
300
]
},
{
"parameters": {
"jsCode": "// Up to 5,000 unique addresses from rows that have no verify_status yet\nconst EMAIL_COLUMN = 'email'; // header of the column that holds the addresses\nconst EMAIL = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9.-]+\\.[a-z]{2,}$/;\nconst batch = new Set();\nfor (const row of $input.all()) {\n if (row.json.verify_status) continue;\n const email = String(row.json[EMAIL_COLUMN] ?? '').trim().toLowerCase();\n if (EMAIL.test(email)) batch.add(email);\n if (batch.size === 5000) break;\n}\nif (batch.size === 0) return []; // nothing left to check: the run ends here\nreturn [{ json: { emails: [...batch], name: 'Google Sheet ' + new Date().toISOString().slice(0, 16) } }];"
},
"id": "22648923-73f7-5ad0-9048-b3c8ab82bceb",
"name": "Pick next batch",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
300
]
},
{
"parameters": {
"method": "POST",
"url": "https://coldleads.app/api/v1/verify/bulk",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ emails: $json.emails, name: $json.name }) }}",
"options": {}
},
"id": "21d28c08-d217-5483-a469-7d8e1bb2b55c",
"name": "Create job",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
660,
300
]
},
{
"parameters": {
"resume": "timeInterval",
"amount": 5,
"unit": "seconds"
},
"id": "027cfbd6-d040-569f-891b-27e82afb10dc",
"name": "Wait 5 s",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
880,
300
],
"webhookId": "bcfa0fa7-0ddb-5ca6-b4cd-8f8683f5d382"
},
{
"parameters": {
"url": "=https://coldleads.app/api/v1/verify/jobs/{{ $('Create job').first().json.id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"id": "c89c51ab-9e45-5c03-a7cd-6e07f77ba4a1",
"name": "Get job status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1100,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "bd05712a-13ee-5582-b4f0-395e0b0fbd68",
"leftValue": "={{ $json.status }}",
"rightValue": "done",
"operator": {
"type": "string",
"operation": "equals"
}
},
{
"id": "704c81e4-6294-54fe-acba-fb6b793532ae",
"leftValue": "={{ $json.status }}",
"rightValue": "failed",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "or"
},
"options": {}
},
"id": "53044579-5bee-56e5-be83-ac265b532b1f",
"name": "Finished?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1320,
300
]
},
{
"parameters": {
"url": "=https://coldleads.app/api/v1/verify/jobs/{{ $('Create job').first().json.id }}?results=1",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"id": "510e5c01-080d-5c15-9efc-217a39166c2f",
"name": "Get results",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1540,
200
]
},
{
"parameters": {
"jsCode": "// Every sheet row whose address is in the job results, duplicates included\nconst EMAIL_COLUMN = 'email';\nconst results = $input.first().json.results ?? [];\nconst byEmail = new Map(results.map((r) => [r.email, r]));\nconst out = [];\nfor (const row of $('Read leads').all()) {\n const r = byEmail.get(String(row.json[EMAIL_COLUMN] ?? '').trim().toLowerCase());\n if (r) {\n out.push({ json: { row_number: row.json.row_number, verify_status: r.status, verify_score: r.score, verify_reason: r.reason } });\n }\n}\nreturn out;"
},
"id": "e2a52d57-ceb5-5001-8583-05108037b70e",
"name": "Match rows",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1760,
200
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "acffb8aa-dc93-5aa6-a4be-faaad80ee30d",
"leftValue": "={{ $json.verify_status }}",
"rightValue": "valid",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "valid"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "1c9e110f-be13-5915-99eb-4effd919e365",
"leftValue": "={{ $json.verify_status }}",
"rightValue": "risky",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "risky"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "d7c0e0eb-7605-5aae-a8ba-44763748f248",
"leftValue": "={{ $json.verify_status }}",
"rightValue": "invalid",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "invalid"
}
]
},
"options": {}
},
"id": "05d0a0b1-98ee-51d2-94a9-70bca0ca8679",
"name": "Route by status",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
1980,
200
]
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit",
"mode": "url"
},
"sheetName": {
"__rl": true,
"value": "Leads",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"row_number": "={{ $json.row_number }}",
"verify_status": "={{ $json.verify_status }}",
"verify_score": "={{ $json.verify_score }}",
"verify_reason": "={{ $json.verify_reason }}",
"next_step": "send"
},
"matchingColumns": [
"row_number"
],
"schema": [
{
"id": "verify_status",
"displayName": "verify_status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "verify_score",
"displayName": "verify_score",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "verify_reason",
"displayName": "verify_reason",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "next_step",
"displayName": "next_step",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"id": "e6d03101-c54b-5b31-a792-fc2ebb2e77b0",
"name": "Write valid",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2200,
60
]
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit",
"mode": "url"
},
"sheetName": {
"__rl": true,
"value": "Leads",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"row_number": "={{ $json.row_number }}",
"verify_status": "={{ $json.verify_status }}",
"verify_score": "={{ $json.verify_score }}",
"verify_reason": "={{ $json.verify_reason }}",
"next_step": "review"
},
"matchingColumns": [
"row_number"
],
"schema": [
{
"id": "verify_status",
"displayName": "verify_status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "verify_score",
"displayName": "verify_score",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "verify_reason",
"displayName": "verify_reason",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "next_step",
"displayName": "next_step",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"id": "2961af6e-c325-5235-a349-13ff161cd9e9",
"name": "Write risky",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2200,
220
]
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit",
"mode": "url"
},
"sheetName": {
"__rl": true,
"value": "Leads",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"row_number": "={{ $json.row_number }}",
"verify_status": "={{ $json.verify_status }}",
"verify_score": "={{ $json.verify_score }}",
"verify_reason": "={{ $json.verify_reason }}",
"next_step": "do not send"
},
"matchingColumns": [
"row_number"
],
"schema": [
{
"id": "verify_status",
"displayName": "verify_status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "verify_score",
"displayName": "verify_score",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "verify_reason",
"displayName": "verify_reason",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "next_step",
"displayName": "next_step",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"id": "521d6f36-f36a-5009-ace8-f22cf8924b43",
"name": "Write invalid",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
2200,
380
]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Read leads",
"type": "main",
"index": 0
}
]
]
},
"Read leads": {
"main": [
[
{
"node": "Pick next batch",
"type": "main",
"index": 0
}
]
]
},
"Pick next batch": {
"main": [
[
{
"node": "Create job",
"type": "main",
"index": 0
}
]
]
},
"Create job": {
"main": [
[
{
"node": "Wait 5 s",
"type": "main",
"index": 0
}
]
]
},
"Wait 5 s": {
"main": [
[
{
"node": "Get job status",
"type": "main",
"index": 0
}
]
]
},
"Get job status": {
"main": [
[
{
"node": "Finished?",
"type": "main",
"index": 0
}
]
]
},
"Finished?": {
"main": [
[
{
"node": "Get results",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait 5 s",
"type": "main",
"index": 0
}
]
]
},
"Get results": {
"main": [
[
{
"node": "Match rows",
"type": "main",
"index": 0
}
]
]
},
"Match rows": {
"main": [
[
{
"node": "Route by status",
"type": "main",
"index": 0
}
]
]
},
"Route by status": {
"main": [
[
{
"node": "Write valid",
"type": "main",
"index": 0
}
],
[
{
"node": "Write risky",
"type": "main",
"index": 0
}
],
[
{
"node": "Write invalid",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
}
}Як працює цикл опитування
- Кожен запит статусу перед відповіддю ще й перевіряє наступну порцію до 25 адрес, а обробник за розкладом просуває відкриті завдання у фоні, тож цикл рухає завдання вперед, а не лише спостерігає за ним.
- Один запит кожні 5 секунд — це 12 на хвилину, значно менше за ліміт у 120 запитів на хвилину на ключ.
- status failed означає, що завдання зупинилося достроково, зазвичай тому, що закінчилися кредити. Отримані до того результати все одно записуються; рядки без результату зберігають порожній verify_status, і їх підхопить наступний запуск.
- За один запуск відкривається лише одне завдання. Акаунт може мати одночасно п’ять завдань у черзі або в роботі, включно із запущеними у веб-застосунку; понад це Create job завершується помилкою 429 too_many_jobs.
Розподіл і запис результатів
| verify_status | next_step | Типові значення verify_reason |
|---|---|---|
| valid | send | ok (поштовий сервер прийняв скриньку) або smtp_unreachable (домен приймає пошту, скриньку не перевірено) |
| risky | review | catch_all, smtp_unknown або одноразовий домен (оцінка 40 чи нижче) |
| invalid | do not send | mailbox_missing, no_mx або syntax |
- Рядки оновлюються за row_number, тож усі дублікати й адреси в різному регістрі в таблиці отримують результат своєї єдиної перевірки.
- Клітинки, що не є e-mail-адресами, ніколи не надсилаються і зберігають порожній verify_status; відфільтруйте їх або виправте в таблиці.
- Щоб обробити кошик, додайте вузли після відповідного виходу Switch, наприклад щоб передавати рядки valid в інструмент для розсилок.
Ліміти та вартість
- 1 кредит за кожну унікальну адресу в завданні, незалежно від результату, зокрема й за результати з 30-денного кешу. Дублікати й клітинки, що не є адресами, прибираються до створення завдання й нічого не коштують.
- До 5 000 адрес у завданні, п’ять відкритих завдань на акаунт, 120 запитів на хвилину на ключ. Результати завдань повертаються лише як JSON.
- Кредити перевіряються під час створення завдання (402 no_credits, якщо баланс менший за завдання) і списуються в міру обробки адрес.
- Тариф Business коштує $99 на місяць і містить API та 10 000 кредитів на місяць; додаткові пакети по 1 000 кредитів коштують $5. Таблиці з 20 000 рядків і 20 000 унікальних адрес потрібно 20 000 кредитів: якщо з місячних 10 000 ще не використано жодного — ці 10 000 плюс 10 пакетів ($50).
Питання
Як перевірити понад 5 000 рядків?
Запустіть workflow знову: кожен запуск бере наступні 5 000 рядків без verify_status. Якщо запускаєте його за розкладом, оберіть інтервал, довший за тривалість завдання, щоб два запуски ніколи не взяли ті самі рядки: за них заплатили б двічі.
Чи є вузол Cold Leads для n8n?
Ні. Workflow використовує стандартний вузол HTTP Request з обліковими даними Header Auth; більше нічого не потрібно.
Чому запуск завершився зі status failed?
Під час завдання в акаунті закінчилися кредити. Уже отримані результати записано; докупіть кредити в Налаштування → Оплата і знову запустіть workflow, щоб обробити решту рядків.
А можна перевіряти по одній адресі?
Так, за допомогою вузла HTTP Request, що викликає POST /api/v1/verify для кожного елемента. Це так само коштує 1 кредит за адресу, але кожна адреса — окремий запит, а ключ дозволяє 120 запитів на хвилину.