CSV- of Excel-contacten in Brevo importeren met een script (Python, Node.js, cURL)
Bulk-import van contacten uit een CSV- of Excel-bestand naar Brevo met de import-contacts API. Inclusief kant-en-klare scripts voor Python en Node.js, een cURL-oneliner, foutafhandeling en tips voor bestanden groter dan 10 MB.
Als je ooit een paar duizend contacten vanuit een spreadsheet naar Brevo hebt moeten pushen, weet je dat de handmatige UI-route snel pijnlijk wordt: bestand kiezen, kolommen mappen, lijst kiezen, wachten, herhalen. Een script doet hetzelfde werk in seconden, en belangrijker: je kunt het opnieuw draaien op een schema, na een database-export, of telkens wanneer je CRM een nieuwe CSV uitspuugt.
Deze gids behandelt het API-endpoint dat Brevo hier precies voor biedt, met volledige werkende scripts in Python, Node.js en een cURL-oneliner. Alles in deze gids gebruikt POST /v3/contacts/import.
Het endpoint in een oogopslag
POST https://api.brevo.com/v3/contacts/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "listIds": [42], "updateExistingContacts": true, "emailBlacklist": false, "smsBlacklist": false}Het antwoord komt snel terug:
{ "processId": 78 }Dat is een 202 Accepted. Brevo heeft de import geaccepteerd en verwerkt deze op de achtergrond. De processId is je tracking-handle als je wilt pollen voor voltooiing of een notifyUrl-webhook wilt instellen.
Een paar dingen om op te letten voordat je code schrijft:
- De body kan CSV (
fileBody), JSON (jsonBody) of een externe URL (fileUrl) zijn. Kies er één. De CSV-vorm gebruikt puntkomma’s (;) om kolommen te scheiden, geen komma’s. Dat is een veelgemaakte fout. fileBodyenjsonBodyzijn beperkt tot 10 MB. Brevo raadt aan rond de 8 MB te blijven voor de zekerheid. Voor alles groter upload je het bestand naar S3, GCS of een andere HTTPS-host en geef jefileUrldoor.- Aangepaste attributen die niet bestaan in je account worden stilzwijgend genegeerd. Maak ze aan in Brevo’s UI (of via de Attributes API) voordat je rijen importeert die ze gebruiken, anders verdwijnt de data gewoon.
updateExistingContactsstaat standaard optrue. Als je het opfalsezet, slaat Brevo contacten over waarvan het e-mailadres al bestaat. Handig voor “alleen nieuwe toevoegen”-jobs.emptyContactsAttributesbepaalt of lege cellen in je CSV bestaande waarden wissen. Standaardfalse(lege cellen worden genegeerd). Zet optrueals je CSV de bron van waarheid is en je blanco’s verouderde data wilt laten wissen.
Een API-sleutel ophalen
Log in bij Brevo, ga naar Settings → SMTP & API → API Keys en maak een nieuwe sleutel aan. Deze ziet eruit als xkeysib-.... Je geeft hem mee als de api-key HTTP-header bij elk verzoek. Behandel hem als een wachtwoord, hij heeft volledige lees/schrijfrechten op je account.
Een nette aanpak: zet hem in een environment variable zodat hij nooit in je broncode terechtkomt.
export BREVO_API_KEY="xkeysib-..."Python: lees een CSV, push naar Brevo
Dit is het simpelst mogelijke script: lees een lokale CSV met de standaardbibliotheek, stuur de body rechtstreeks naar Brevo. Geen externe SDK nodig behalve requests.
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}")Draai het:
python import_csv_to_brevo.py contacts.csvDe eerste rij van je CSV is de header. Kolomnamen mappen naar Brevo-attributen op basis van hoofdletters, exacte match: EMAIL, FIRSTNAME, LASTNAME, plus elk aangepast attribuut dat je hebt gedefinieerd. EMAIL is verplicht.
EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITYDat is de hele flow. Het script keert binnen een seconde terug; de daadwerkelijke import draait server-side en duurt van een paar seconden tot een paar minuten, afhankelijk van het volume.
Excel-bestanden: drie haalbare paden
Brevo’s import-endpoint leest geen .xlsx rechtstreeks, dus je hebt drie echte opties afhankelijk van waar het werk moet plaatsvinden:
- Een VBA-macro draaien in de werkmap. Een-klik-sync vanuit Excel zelf, geen extern script. Dit is het juiste antwoord wanneer het bestand op een desktop staat en je team een knop op het blad wil. Volledige code in de Excel-naar-Brevo VBA-macrogids.
- Office Scripts + Power Automate. Als de werkmap in OneDrive/SharePoint staat en je onbeheerde geplande synchronisatie wilt. Ook behandeld in de Excel-gids.
- Converteer
.xlsxnaar CSV vanuit een script. Wat de rest van deze sectie behandelt. Het beste als je al Python of Node op een server draait en gewoon één keer per dag een werkmap moet binnenhalen.
Voor optie 3 maakt pandas er een oneliner van:
# 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")Als je pandas niet als afhankelijkheid wilt, werkt openpyxl alleen ook:
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: hetzelfde werk, officiële SDK
Brevo publiceert @getbrevo/brevo voor Node. Het regelt authenticatie, retries en de getypeerde request-vorm:
// 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}`);Of, als je de SDK niet wilt, werkt gewone fetch op dezelfde manier:
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: een oneliner voor ad-hoc imports
Wanneer je gewoon het endpoint wilt testen of handmatig een klein bestand erin wilt duwen:
# 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 . slurpt het hele bestand op en JSON-escaped het. Dat scheelt je het handmatig escapen van newlines en quotes.
Bestanden groter dan 10 MB: gebruik fileUrl
fileBody is beperkt tot 10 MB. Als je contactdump groter is, host je het bestand op een URL die Brevo kan ophalen en geef je die door:
body = { "fileUrl": "https://files.example.com/contacts/2026-04-30.csv", "listIds": [42], "updateExistingContacts": True,}Alles wat bereikbaar is via een publieke HTTPS-URL werkt: pre-signed S3-URL’s, GCS, je eigen statische host, zelfs een GitHub raw URL voor eenmalige imports. Brevo haalt het bestand op vanaf jouw URL en draait dan de import. Geaccepteerde formaten: .csv, .txt, .json.
Stuur JSON in plaats van CSV
Als je al contacten uit een database trekt, hoef je niet via CSV te gaan. Stuur ze direct als JSON:
body = { "jsonBody": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "attributes": { "FIRSTNAME": "John", "LASTNAME": "Smith", "COMPANY": "Globex", }, }, ], "listIds": [42],}Dezelfde 10 MB-limiet. Hetzelfde async-gedrag.
Pollen voor voltooiing
De import is asynchroon. processId is je handle om hem te volgen. Er is een apart endpoint om de processtatus te controleren:
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}")Voor langlopende jobs is het schonere patroon notifyUrl: geef een HTTPS-endpoint door waar Brevo naar zal POSTen wanneer de import is voltooid, en sla het pollen over.
body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"Veelvoorkomende fouten en hoe je ze oplost
400 Bad Request zonder duidelijke oorzaak. Bijna altijd puntkomma’s versus komma’s in de CSV. Brevo’s import verwacht ;, niet ,. Controleer fileBody na je conversiestap.
Aangepaste attribuutwaarden verdwijnen. Het attribuut bestaat niet in je account. Maak het aan onder Contacts → Settings → Contact attributes voordat je importeert, of gebruik de Attributes API om het aan te maken als onderdeel van je script.
401 Unauthorized. Verkeerde headernaam. Het is api-key (kleine letters, koppelteken), niet Authorization of X-API-Key.
Import “geslaagd” maar contacten verschenen niet in de lijst. Controleer dat listIds de juiste lijst bevat. Ook: als updateExistingContacts op false staat en de contacten al bestaan, slaat Brevo ze stilzwijgend over in plaats van ze opnieuw aan de lijst toe te voegen.
Sommige rijen geïmporteerd, sommige niet. Brevo e-mailt je een per-rij foutrapport nadat de import is voltooid (tenzij je disableNotification: true instelt). Het rapport vertelt je welke rijen ongeldige e-mails, ontbrekende verplichte velden of opmaakproblemen hadden.
Wanneer scripten versus wanneer de UI gebruiken
De UI is prima voor eenmalige imports onder de duizend rijen. Een script wint zodra:
- Je vaker dan één keer importeert (bijvoorbeeld wekelijkse export uit je CRM)
- De brondata schoongemaakt moet worden voordat het in Brevo terechtkomt (deduplicatie, telefoonnummers formatteren, volledige namen splitsen in voor- en achternaam)
- Je wilt dat het op een schema draait zonder dat iemand op knoppen klikt
- Het bestand groter is dan de UI comfortabel aankan
Wikkel het bovenstaande script in een cronjob of een GitHub Action en je hebt geautomatiseerde contactsync. De volgende post in deze serie laat zien hoe je hetzelfde direct vanuit een Google Sheet doet met Apps Script. Geen server nodig.