Bulk-Prüfung von E-Mails in n8n mit Google Sheets
- Für wen
- n8n-Nutzer mit Lead-Listen in Google Sheets
- Das Problem
- Eine Prüf-API einmal pro Zeile aufzurufen ist langsam und stößt an Ratenlimits, eine Bulk-API dagegen braucht einen Auftrag, eine Polling-Schleife und einen zuverlässigen Weg, die Ergebnisse den richtigen Zeilen zuzuordnen.
- Die Lösung
- Ein Workflow nimmt bis zu 5.000 Zeilen, die noch kein Ergebnis haben, sendet ihre eindeutigen Adressen als einen einzigen Cold-Leads-Bulk-Auftrag, fragt den Auftrag alle 5 Sekunden ab, holt die Ergebnisse, verteilt jede Zeile nach Status und schreibt Status, Score, Grund und einen nächsten Schritt in das Sheet. Für die nächsten 5.000 führen Sie ihn erneut aus.
- Was Sie bekommen
- Vier ausgefüllte Spalten pro Zeile (verify_status, verify_score, verify_reason, next_step) und ein Workflow, den Sie erneut ausführen können, bis das ganze Sheet fertig ist.
Adressen, IDs und Ergebnisse in den Beispielen sind illustrativ. example.com ist für Dokumentation reserviert, eine echte Prüfung dieser Adressen liefert daher invalid.
Die Codebeispiele sind in allen Sprachen gleich, ihre Kommentare sind auf Englisch.
Was der Workflow tut
- Read leads (Google Sheets): alle Zeilen des Sheets, jeweils mit ihrer row_number.
- Pick next batch (Code): Zeilen ohne verify_status, Adressen von Leerzeichen befreit und kleingeschrieben, Duplikate und Zellen, die keine E-Mail-Adressen sind, entfernt, höchstens 5.000. Bleibt nichts übrig, endet der Workflow hier.
- Create job (HTTP Request): POST /api/v1/verify/bulk mit den Adressen. Die Antwort ist 202 mit der Auftrags-ID.
- Wait 5 s, dann Get job status (HTTP Request): GET /api/v1/verify/jobs/{id}. Finished? (If) schickt den Ablauf zurück zum Warten, bis der Status done oder failed ist.
- Get results (HTTP Request): dieselbe Anfrage mit results=1, die einen Eintrag pro Adresse ergänzt: email, status, score und reason.
- Match rows (Code): jede Zeile des Sheets, deren Adresse in den Ergebnissen steht, einschließlich Duplikaten, mit ihrer row_number.
- Route by status (Switch): valid, risky und invalid gehen jeweils an ein eigenes Google-Sheets-Update, das die drei Ergebnisspalten und next_step schreibt (send, review oder do not send).
Sheet und Zugangsdaten vorbereiten
- Das Sheet braucht eine Kopfzeile mit den Spalten email, verify_status, verify_score, verify_reason und next_step (andere Spalten können bleiben).
- Ein Google-Sheets-Credential in n8n mit Zugriff auf die Tabelle.
- Ein Header-Auth-Credential für Cold Leads: Name x-api-key, Value Ihr geheimer Schlüssel (sk_…) aus Einstellungen → API-Schlüssel. Die API ist Teil des Business-Tarifs.
Der Workflow
Kopieren Sie das JSON und fügen Sie es in einen leeren n8n-Canvas ein, oder speichern Sie es als Datei und importieren Sie es. Öffnen Sie dann die vier Google-Sheets-Nodes, ersetzen Sie die Tabellen-URL und den Blattnamen (Leads) durch Ihre eigenen und wählen Sie Ihr Google-Credential aus; in den drei HTTP-Request-Nodes wählen Sie das Header-Auth-Credential für 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"
}
}Wie die Polling-Schleife funktioniert
- Jede Statusanfrage prüft vor ihrer Antwort auch den nächsten Stapel von bis zu 25 Adressen, und ein geplanter Worker treibt offene Aufträge im Hintergrund voran; die Schleife bringt den Auftrag also weiter, statt ihn nur zu beobachten.
- Eine Anfrage alle 5 Sekunden ergibt 12 pro Minute, weit unter dem Limit von 120 Anfragen pro Minute und Schlüssel.
- status failed bedeutet, dass der Auftrag vorzeitig beendet wurde, meist weil die Credits aufgebraucht waren. Seine bisherigen Ergebnisse werden trotzdem geschrieben; Zeilen ohne Ergebnis behalten ein leeres verify_status und werden beim nächsten Lauf aufgegriffen.
- Pro Lauf ist nur ein Auftrag offen. Ein Konto darf fünf wartende oder laufende Aufträge gleichzeitig haben, einschließlich in der Web-App gestarteter Aufträge; darüber hinaus schlägt Create job mit 429 too_many_jobs fehl.
Verteilung und Zurückschreiben
| verify_status | next_step | Typische Werte für verify_reason |
|---|---|---|
| valid | send | ok (ein Mailserver hat das Postfach akzeptiert) oder smtp_unreachable (die Domain nimmt E-Mails an, das Postfach wurde nicht geprüft) |
| risky | review | catch_all, smtp_unknown oder eine Wegwerf-Domain (Score 40 oder niedriger) |
| invalid | do not send | mailbox_missing, no_mx oder syntax |
- Zeilen werden über row_number aktualisiert, sodass Duplikate und Adressen mit gemischter Groß- und Kleinschreibung im Sheet alle das Ergebnis ihrer einen Prüfung erhalten.
- Zellen, die keine E-Mail-Adressen sind, werden nie gesendet und behalten ein leeres verify_status; filtern Sie sie heraus oder korrigieren Sie sie im Sheet.
- Um mit einem Bereich weiterzuarbeiten, hängen Sie Nodes an seinen Switch-Ausgang, zum Beispiel um valid-Zeilen in ein Kampagnen-Tool zu übernehmen.
Limits und Kosten
- 1 Credit pro eindeutiger Adresse in einem Auftrag, unabhängig vom Ergebnis, auch für Ergebnisse aus dem 30-Tage-Cache. Duplikate und Zellen, die keine Adressen sind, werden vor dem Anlegen des Auftrags entfernt und kosten nichts.
- Bis zu 5.000 Adressen pro Auftrag, fünf offene Aufträge pro Konto, 120 Anfragen pro Minute und Schlüssel. Auftragsergebnisse kommen nur als JSON zurück.
- Credits werden beim Anlegen des Auftrags geprüft (402 no_credits, wenn das Guthaben kleiner als der Auftrag ist) und abgebucht, während die Adressen verarbeitet werden.
- Der Business-Tarif kostet $99 im Monat und enthält die API und 10.000 Credits im Monat; zusätzliche Pakete zu 1.000 Credits kosten $5. Ein Sheet mit 20.000 Zeilen und 20.000 eindeutigen Adressen braucht 20.000 Credits: Wenn von den 10.000 des Monats noch keine verbraucht sind, diese plus 10 Pakete ($50).
FAQ
Wie prüfe ich mehr als 5.000 Zeilen?
Führen Sie den Workflow erneut aus: Jeder Lauf nimmt die nächsten 5.000 Zeilen ohne verify_status. Wenn Sie ihn zeitgesteuert ausführen, wählen Sie ein Intervall, das länger ist, als ein Auftrag dauert, damit nie zwei Läufe dieselben Zeilen aufgreifen; sie würden doppelt berechnet.
Gibt es einen Cold-Leads-Node für n8n?
Nein. Der Workflow nutzt den Standard-Node HTTP Request mit einem Header-Auth-Credential; mehr braucht es nicht.
Warum endete ein Lauf mit dem Status failed?
Dem Konto sind während des Auftrags die Credits ausgegangen. Die vorhandenen Ergebnisse werden geschrieben; kaufen Sie unter Einstellungen → Abrechnung Credits nach und führen Sie den Workflow erneut aus, um die restlichen Zeilen abzuschließen.
Kann ich Adressen stattdessen einzeln prüfen?
Ja, mit einem HTTP-Request-Node, der POST /api/v1/verify für jedes Item aufruft. Das kostet ebenfalls 1 Credit pro Adresse, aber jede Adresse ist eine eigene Anfrage, und ein Schlüssel erlaubt 120 Anfragen pro Minute.