CSV- oder Excel-Kontakte mit einem Skript zu Brevo importieren (Python, Node.js, cURL)
Importiere Kontakte aus einer CSV- oder Excel-Datei per Brevo-API (import-contacts) im Bulk. Inklusive sofort einsetzbarer Skripte in Python und Node.js, cURL-Einzeiler, Fehlerbehandlung und Tipps für Dateien über 10 MB.
Wenn du schon mal ein paar tausend Kontakte aus einer Tabelle in Brevo schieben musstest, kennst du den manuellen UI-Weg: Datei wählen, Spalten zuordnen, Liste auswählen, warten, wiederholen. Das wird schnell zäh. Ein Skript erledigt dieselbe Aufgabe in Sekunden, und (was wichtiger ist) du kannst es geplant erneut laufen lassen, nach einem Datenbank-Export oder immer dann, wenn dein CRM eine frische CSV ausspuckt.
Diese Anleitung deckt genau den API-Endpoint ab, den Brevo dafür vorsieht, mit voll funktionierenden Skripten in Python, Node.js und einem cURL-Einzeiler. Alles in dieser Anleitung läuft über POST /v3/contacts/import.
Der Endpoint im Überblick
POST https://api.brevo.com/v3/contacts/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "listIds": [42], "updateExistingContacts": true, "emailBlacklist": false, "smsBlacklist": false}Die Antwort kommt schnell zurück:
{ "processId": 78 }Das ist ein 202 Accepted. Brevo hat den Import angenommen und verarbeitet ihn im Hintergrund. Die processId ist dein Tracking-Handle, falls du den Status abfragen oder einen notifyUrl-Webhook einrichten willst.
Ein paar Punkte, die du vor dem Coden kennen solltest:
- Der Body kann CSV (
fileBody), JSON (jsonBody) oder eine Remote-URL (fileUrl) sein. Wähle eine Variante. Die CSV-Form trennt Spalten mit Semikolons (;), nicht mit Kommas, das ist ein häufiger Stolperstein. fileBodyundjsonBodysind auf 10 MB gedeckelt. Brevo empfiehlt, sicherheitshalber bei rund 8 MB zu bleiben. Für alles Größere lade die Datei zu S3, GCS oder einem beliebigen HTTPS-Host und übergib stattdessenfileUrl.- Benutzerdefinierte Attribute, die nicht in deinem Konto existieren, werden stillschweigend ignoriert. Lege sie in der Brevo-Oberfläche (oder über die Attributes API) an, bevor du Zeilen importierst, die sie verwenden, sonst verschwinden die Daten einfach.
updateExistingContactsist standardmäßigtrue. Wenn du es auffalsesetzt, überspringt Brevo Kontakte, deren E-Mail bereits existiert, nützlich für Jobs, die nur neue Kontakte hinzufügen sollen.emptyContactsAttributessteuert, ob leere Zellen in deiner CSV bestehende Werte löschen. Standardfalse(leere Zellen werden ignoriert). Setze es auftrue, wenn deine CSV die Wahrheit ist und leere Felder veraltete Daten überschreiben sollen.
API-Schlüssel besorgen
Melde dich in Brevo an → Einstellungen → SMTP & API → API Keys → erstelle einen neuen Schlüssel. Er sieht aus wie xkeysib-.... Du übergibst ihn als HTTP-Header api-key bei jeder Anfrage. Behandle ihn wie ein Passwort, er hat vollen Lese- und Schreibzugriff auf dein Konto.
Saubere Praxis: leg ihn in eine Umgebungsvariable, damit er nie in deinem Quellcode landet.
export BREVO_API_KEY="xkeysib-..."Python: CSV einlesen, an Brevo schicken
Das ist das einfachste Skript, das man bauen kann: lokale CSV mit der Standardbibliothek lesen, Body direkt an Brevo schicken. Außer requests brauchst du kein Drittanbieter-SDK.
import csvimport ioimport osimport sysimport requests
API_KEY = os.environ["BREVO_API_KEY"]LIST_ID = 42 # the Brevo list to add contacts toINPUT_FILE = sys.argv[1] if len(sys.argv) > 1 else "contacts.csv"
# Brevo wants semicolon-separated CSV. If your file uses commas (most do),# convert it on the fly so you don't have to edit the source spreadsheet.def to_brevo_csv(path: str) -> str: with open(path, newline="", encoding="utf-8") as f: reader = csv.reader(f) # default: comma-separated out = io.StringIO() writer = csv.writer(out, delimiter=";") for row in reader: writer.writerow(row) return out.getvalue()
body = { "fileBody": to_brevo_csv(INPUT_FILE), "listIds": [LIST_ID], "updateExistingContacts": True, "emptyContactsAttributes": False, "emailBlacklist": False, "smsBlacklist": False,}
resp = requests.post( "https://api.brevo.com/v3/contacts/import", json=body, headers={"api-key": API_KEY, "Content-Type": "application/json"}, timeout=60,)
if resp.status_code != 202: print(f"Import failed: {resp.status_code} {resp.text}") sys.exit(1)
process_id = resp.json()["processId"]print(f"Import accepted. Process ID: {process_id}")Ausführen:
python import_csv_to_brevo.py contacts.csvDie erste Zeile deiner CSV ist der Header. Spaltennamen werden den Brevo-Attributen per Großbuchstaben, exakter Übereinstimmung zugeordnet: EMAIL, FIRSTNAME, LASTNAME plus jedes benutzerdefinierte Attribut, das du angelegt hast. EMAIL ist Pflicht.
EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITYDas ist der ganze Ablauf. Das Skript kommt innerhalb einer Sekunde zurück, der eigentliche Import läuft serverseitig und dauert je nach Volumen ein paar Sekunden bis ein paar Minuten.
Excel-Dateien: drei realistische Wege
Der Import-Endpoint von Brevo liest .xlsx nicht direkt, also hast du drei echte Optionen, je nachdem, wo die Arbeit stattfinden soll:
- Ein VBA-Makro in der Arbeitsmappe – ein-Klick-Sync direkt aus Excel, ohne externes Skript. Die richtige Antwort, wenn die Datei auf einem Desktop liegt und dein Team einen Button auf dem Sheet möchte. Vollständiger Code in der Excel-zu-Brevo-VBA-Makro-Anleitung.
- Office Scripts + Power Automate – wenn die Arbeitsmappe in OneDrive/SharePoint liegt und du eine geplante, unbeaufsichtigte Synchronisation willst. Wird ebenfalls in der Excel-Anleitung behandelt.
.xlsxper Skript in CSV umwandeln – worum es im Rest dieses Abschnitts geht. Am besten, wenn du ohnehin Python oder Node auf einem Server laufen lässt und einfach einmal am Tag eine Arbeitsmappe einziehen willst.
Für Option 3 macht pandas das in einer Zeile:
# pip install pandas openpyxlimport pandas as pd
def xlsx_to_brevo_csv(path: str, sheet_name: str | int = 0) -> str: df = pd.read_excel(path, sheet_name=sheet_name) return df.to_csv(sep=";", index=False)
body["fileBody"] = xlsx_to_brevo_csv("contacts.xlsx")Wenn du pandas nicht als Abhängigkeit willst, reicht auch openpyxl allein:
from openpyxl import load_workbookimport csv, io
def xlsx_to_brevo_csv(path: str) -> str: wb = load_workbook(path, read_only=True) ws = wb.active out = io.StringIO() writer = csv.writer(out, delimiter=";") for row in ws.iter_rows(values_only=True): writer.writerow(["" if v is None else v for v in row]) return out.getvalue()Node.js: derselbe Job, offizielles SDK
Brevo veröffentlicht @getbrevo/brevo für Node. Es kümmert sich um Auth, Retries und die typisierte Request-Struktur:
// npm install @getbrevo/brevoimport fs from 'node:fs/promises';import { BrevoClient } from '@getbrevo/brevo';
const API_KEY = process.env.BREVO_API_KEY;const LIST_ID = 42;const INPUT_FILE = process.argv[2] ?? 'contacts.csv';
const client = new BrevoClient({ apiKey: API_KEY });
// Read the CSV and convert commas to semicolons if neededconst raw = await fs.readFile(INPUT_FILE, 'utf-8');const csv = raw.includes(';') ? raw : raw.replace(/,/g, ';');
const result = await client.contacts.importContacts({ fileBody: csv, listIds: [LIST_ID], updateExistingContacts: true, emptyContactsAttributes: false,});
console.log(`Import accepted. Process ID: ${result.processId}`);Oder, wenn du das SDK nicht willst, klappt schlichtes fetch genauso:
const resp = await fetch('https://api.brevo.com/v3/contacts/import', { method: 'POST', headers: { 'api-key': process.env.BREVO_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ fileBody: csv, listIds: [42], updateExistingContacts: true, }),});
if (resp.status !== 202) { throw new Error(`Import failed: ${resp.status} ${await resp.text()}`);}
const { processId } = await resp.json();console.log(`Process ID: ${processId}`);cURL: Einzeiler für Ad-hoc-Importe
Wenn du den Endpoint nur testen oder eine kleine Datei manuell durchschieben willst:
# Convert commas to semicolons, then send the file inlinecsv=$(sed 's/,/;/g' contacts.csv | jq -Rs .)
curl -X POST https://api.brevo.com/v3/contacts/import \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"fileBody\": $csv, \"listIds\": [42], \"updateExistingContacts\": true}"jq -Rs . schluckt die ganze Datei und JSON-escaped sie, das erspart dir das manuelle Escapen von Zeilenumbrüchen und Anführungszeichen.
Dateien größer als 10 MB: fileUrl verwenden
fileBody ist auf 10 MB gedeckelt. Wenn dein Kontaktdump größer ist, hoste die Datei unter einer URL, die Brevo abrufen kann, und übergib sie:
body = { "fileUrl": "https://files.example.com/contacts/2026-04-30.csv", "listIds": [42], "updateExistingContacts": True,}Alles, was über eine öffentliche HTTPS-URL erreichbar ist, funktioniert: vorsignierte S3-URLs, GCS, dein eigener Static Host, sogar eine GitHub-Raw-URL für einmalige Importe. Brevo holt die Datei von deiner URL und führt dann den Import aus. Akzeptierte Formate: .csv, .txt, .json.
JSON statt CSV senden
Wenn du Kontakte ohnehin schon aus einer Datenbank ziehst, brauchst du keinen Umweg über CSV, schick sie direkt als JSON:
body = { "jsonBody": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "attributes": { "FIRSTNAME": "John", "LASTNAME": "Smith", "COMPANY": "Globex", }, }, ], "listIds": [42],}Gleiches 10-MB-Limit. Gleiches asynchrones Verhalten.
Status abfragen
Der Import ist asynchron, processId ist dein Handle, um ihn zu verfolgen. Es gibt einen separaten Endpoint zum Prüfen des Prozessstatus:
def wait_for_import(process_id: int, timeout_s: int = 600) -> dict: import time deadline = time.time() + timeout_s while time.time() < deadline: r = requests.get( f"https://api.brevo.com/v3/processes/{process_id}", headers={"api-key": API_KEY}, timeout=30, ) r.raise_for_status() status = r.json() if status["status"] in ("completed", "failed"): return status time.sleep(5) raise TimeoutError(f"Import {process_id} did not finish in {timeout_s}s")
result = wait_for_import(process_id)print(f"Import {result['status']}: {result}")Für lange laufende Jobs ist notifyUrl das sauberere Muster: gib einen HTTPS-Endpoint an, an den Brevo POSTet, sobald der Import fertig ist, und spar dir das Polling.
body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"Häufige Fehler und wie du sie behebst
400 Bad Request ohne offensichtlichen Grund. Fast immer Semikolons gegen Kommas in der CSV. Brevos Import erwartet ;, nicht ,. Prüfe fileBody nach deinem Konvertierungsschritt nochmal.
Werte für benutzerdefinierte Attribute verschwinden. Das Attribut existiert nicht in deinem Konto. Lege es unter Kontakte → Einstellungen → Kontakt-Attribute an, bevor du importierst, oder nutze die Attributes API, um es als Teil deines Skripts zu erzeugen.
401 Unauthorized. Falscher Header-Name. Es ist api-key (kleingeschrieben, mit Bindestrich), nicht Authorization oder X-API-Key.
Import “erfolgreich”, aber Kontakte tauchen nicht in der Liste auf. Prüfe, ob listIds die richtige Liste enthält. Außerdem: wenn updateExistingContacts auf false steht und die Kontakte bereits existieren, überspringt Brevo sie still, statt sie der Liste erneut hinzuzufügen.
Manche Zeilen importiert, manche nicht. Brevo schickt dir nach dem Import einen Fehlerbericht pro Zeile per E-Mail (außer du setzt disableNotification: true). Der Bericht sagt dir, welche Zeilen ungültige E-Mails, fehlende Pflichtfelder oder Formatierungsprobleme hatten.
Wann skripten, wann die UI nutzen
Die UI ist okay für einmalige Importe unter tausend Zeilen. Ein Skript gewinnt, sobald:
- Du mehr als einmal importierst (z. B. wöchentlicher Export aus deinem CRM)
- Die Quelldaten vor dem Import in Brevo bereinigt werden müssen (Deduplizierung, Telefonnummern formatieren, vollständige Namen in Vor- und Nachname aufteilen)
- Du es nach Plan laufen lassen willst, ohne dass jemand klickt
- Die Datei größer ist, als die UI komfortabel handhabt
Pack das obige Skript in einen Cronjob oder eine GitHub Action, und du hast eine automatisierte Kontaktsynchronisation. Der nächste Beitrag in dieser Reihe zeigt, wie du dasselbe direkt aus einem Google Sheet mit Apps Script machst, ohne Server.