Hromadné ověřování e-mailů v n8n s Google Sheets
- Pro koho
- Uživatelé n8n se seznamy leadů v Google Sheets
- Problém
- Volat ověřovací API pro každý řádek zvlášť je pomalé a naráží na limity požadavků, zatímco hromadné API potřebuje úlohu, smyčku dotazů na stav a spolehlivý způsob, jak výsledky přiřadit zpět ke správným řádkům.
- Řešení
- Jedno workflow vezme až 5 000 řádků, které ještě nemají výsledek, pošle jejich unikátní adresy jako jednu hromadnou úlohu Cold Leads, každých 5 sekund se dotáže na její stav, stáhne výsledky, každý řádek nasměruje podle stavu a do listu zapíše stav, skóre, důvod a další krok. Pro dalších 5 000 ho spusťte znovu.
- Co získáte
- Čtyři vyplněné sloupce v každém řádku (verify_status, verify_score, verify_reason, next_step) a workflow, které můžete spouštět znovu, dokud nebude hotový celý list.
Adresy, identifikátory a výsledky v příkladech jsou ilustrativní. Doména example.com je vyhrazená pro dokumentaci, takže skutečná kontrola těchto adres vrátí invalid.
Ukázky kódu jsou ve všech jazycích stejné, komentáře v nich jsou anglicky.
Co workflow dělá
- Read leads (Google Sheets): všechny řádky listu, každý se svým row_number.
- Pick next batch (Code): řádky bez verify_status, adresy bez okrajových mezer a převedené na malá písmena, bez duplicit a bez buněk, které nejsou e-mailovými adresami, nejvýš 5 000. Pokud nic nezbývá, workflow tady skončí.
- Create job (HTTP Request): POST /api/v1/verify/bulk s adresami. Odpověď je 202 s id úlohy.
- Wait 5 s, pak Get job status (HTTP Request): GET /api/v1/verify/jobs/{id}. Finished? (If) vrací tok zpět k čekání, dokud stav není done nebo failed.
- Get results (HTTP Request): stejný požadavek s results=1, který přidá jednu položku na adresu: email, status, score a reason.
- Match rows (Code): každý řádek listu, jehož adresa je ve výsledcích, včetně duplicit, s jeho row_number.
- Route by status (Switch): větve valid, risky a invalid vedou každá do vlastní aktualizace Google Sheets, která zapíše tři sloupce s výsledkem a next_step (send, review nebo do not send).
Připravte list a přihlašovací údaje
- List potřebuje řádek záhlaví se sloupci email, verify_status, verify_score, verify_reason a next_step (ostatní sloupce mohou zůstat).
- Přihlašovací údaje (credential) Google Sheets v n8n s přístupem k tabulce.
- Přihlašovací údaje Header Auth pro Cold Leads: Name x-api-key, Value váš tajný klíč (sk_…) z Nastavení → API klíče. API je součástí tarifu Business.
Workflow
Zkopírujte JSON a vložte ho na prázdné plátno n8n, nebo ho uložte jako soubor a naimportujte. Pak otevřete čtyři uzly Google Sheets, nahraďte URL tabulky a název listu (Leads) svými a vyberte své přihlašovací údaje Google, a ve třech uzlech HTTP Request vyberte přihlašovací údaje Header Auth pro 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"
}
}Jak funguje smyčka dotazů na stav
- Každý dotaz na stav navíc před odpovědí ověří další dávku až 25 adres a plánovaný worker posouvá otevřené úlohy na pozadí, takže smyčka úlohu posouvá kupředu, místo aby ji jen sledovala.
- Jeden požadavek každých 5 sekund je 12 za minutu, hluboko pod limitem 120 požadavků za minutu na klíč.
- Stav failed znamená, že se úloha zastavila předčasně, obvykle proto, že došly kredity. Dosavadní výsledky se přesto zapíšou; řádky bez výsledku si ponechají prázdný verify_status a vezme je další spuštění.
- Jedno spuštění má otevřenou jen jednu úlohu. Účet může mít najednou pět úloh ve frontě nebo v běhu, včetně úloh spuštěných ve webové aplikaci; nad tento počet Create job selže s 429 too_many_jobs.
Směrování a zápis zpět
| verify_status | next_step | Typické hodnoty verify_reason |
|---|---|---|
| valid | send | ok (poštovní server schránku přijal) nebo smtp_unreachable (doména přijímá poštu, schránka se nekontrolovala) |
| risky | review | catch_all, smtp_unknown nebo jednorázová doména (skóre 40 nebo nižší) |
| invalid | do not send | mailbox_missing, no_mx nebo syntax |
- Řádky se aktualizují podle row_number, takže duplicity i adresy s různou velikostí písmen v listu dostanou výsledek jediné kontroly své adresy.
- Buňky, které nejsou e-mailovými adresami, se nikdy neodesílají a ponechají si prázdný verify_status; odfiltrujte je nebo je v listu opravte.
- Chcete-li s některou skupinou dál pracovat, přidejte uzly za její výstup ze Switch, například pro přidání platných řádků do nástroje pro kampaně.
Limity a náklady
- 1 kredit za každou unikátní adresu v úloze bez ohledu na výsledek, včetně výsledků z 30denní cache. Duplicity a buňky, které nejsou adresami, se odstraní před vytvořením úlohy a nic nestojí.
- Až 5 000 adres na úlohu, pět otevřených úloh na účet, 120 požadavků za minutu na klíč. Výsledky úloh přicházejí jen jako JSON.
- Kredity se kontrolují při vytvoření úlohy (402 no_credits, pokud je zůstatek menší než úloha) a strhávají se průběžně, jak se adresy zpracovávají.
- Tarif Business stojí $99 měsíčně a zahrnuje API a 10 000 kreditů měsíčně; další balíčky po 1 000 kreditech stojí $5. List s 20 000 řádky a 20 000 unikátními adresami potřebuje 20 000 kreditů: pokud z 10 000 měsíčních kreditů ještě žádný nebyl využit, jsou to těchto 10 000 a k tomu 10 balíčků ($50).
Otázky
Jak ověřit víc než 5 000 řádků?
Spusťte workflow znovu: každé spuštění vezme dalších 5 000 řádků bez verify_status. Pokud ho plánujete, zvolte interval delší, než trvá jedna úloha, aby dvě spuštění nikdy nevzala stejné řádky; ty by se účtovaly dvakrát.
Existuje pro n8n uzel Cold Leads?
Ne. Workflow používá standardní uzel HTTP Request s přihlašovacími údaji Header Auth; nic dalšího není potřeba.
Proč spuštění skončilo stavem failed?
Účtu během úlohy došly kredity. Výsledky, které úloha měla, se zapíšou; dokupte kredity v Nastavení → Platby a spusťte workflow znovu, aby dokončilo zbývající řádky.
Můžu místo toho kontrolovat adresy po jedné?
Ano, uzlem HTTP Request, který volá POST /api/v1/verify pro každou položku. Stojí to stejně 1 kredit za adresu, ale každá adresa je samostatný požadavek a klíč povoluje 120 požadavků za minutu.