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.

Featured image for article: CSV- oder Excel-Kontakte mit einem Skript zu Brevo importieren (Python, Node.js, cURL)

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/import
Content-Type: application/json
api-key: YOUR_API_KEY
{
"fileBody": "EMAIL;FIRSTNAME;LASTNAME\n[email protected];Jane;Doe",
"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.
  • fileBody und jsonBody sind 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 stattdessen fileUrl.
  • 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.
  • updateExistingContacts ist standardmäßig true. Wenn du es auf false setzt, überspringt Brevo Kontakte, deren E-Mail bereits existiert, nützlich für Jobs, die nur neue Kontakte hinzufügen sollen.
  • emptyContactsAttributes steuert, ob leere Zellen in deiner CSV bestehende Werte löschen. Standard false (leere Zellen werden ignoriert). Setze es auf true, 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.

Terminal window
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_csv_to_brevo.py
import csv
import io
import os
import sys
import requests
API_KEY = os.environ["BREVO_API_KEY"]
LIST_ID = 42 # the Brevo list to add contacts to
INPUT_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:

Terminal window
python import_csv_to_brevo.py contacts.csv

Die 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,CITY
[email protected],Jane,Doe,Acme,Berlin
[email protected],John,Smith,Globex,Paris

Das 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:

  1. 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.
  2. 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.
  3. .xlsx per 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 openpyxl
import 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_workbook
import 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:

import-csv-to-brevo.mjs
// npm install @getbrevo/brevo
import 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 needed
const 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:

Terminal window
# Convert commas to semicolons, then send the file inline
csv=$(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": [
{
"email": "[email protected]",
"attributes": {
"FIRSTNAME": "Jane",
"LASTNAME": "Doe",
"COMPANY": "Acme",
},
},
{
"email": "[email protected]",
"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.

Frequently Asked Questions

Welcher Brevo-API-Endpoint ist für den Bulk-Kontaktimport gedacht?
POST https://api.brevo.com/v3/contacts/import. Er akzeptiert CSV-Inhalte (fileBody), eine Remote-Datei-URL (fileUrl) oder ein JSON-Array (jsonBody). Der Endpoint arbeitet asynchron, er liefert sofort eine processId zurück und schließt den Import im Hintergrund ab.
Wie groß darf die Datei höchstens sein?
10 MB für inline-CSV (fileBody) oder JSON (jsonBody). Bei größeren Dateien hostest du die Datei an einem erreichbaren Ort und übergibst die URL stattdessen über fileUrl. Dieser Weg hat keine dokumentierte Größenbeschränkung.
Wie importiere ich eine .xlsx-Datei?
Die Import-API von Brevo akzeptiert nur .csv, .txt oder .json. Wandle die .xlsx vorher in CSV um, das schaffen pandas, openpyxl oder LibreOffice headless mit einer einzigen Zeile. Das Skript in dieser Anleitung enthält eine xlsx-zu-csv-Konvertierung.
Werden vorhandene Kontakte überschrieben?
Standardmäßig ja, updateExistingContacts ist auf true gesetzt und gleicht über die E-Mail ab. Setze den Wert auf false, um bestehende Kontakte zu überspringen. Setze emptyContactsAttributes auf true, wenn leere CSV-Zellen vorhandene Werte löschen sollen (sonst werden leere Zellen ignoriert).
Kostenlos mit Brevo starten