Comment importer des contacts CSV ou Excel dans Brevo avec un script (Python, Node.js, cURL)
Importez en masse les contacts d'un fichier CSV ou Excel dans Brevo via l'API import-contacts. Inclut des scripts Python et Node.js prêts à l'emploi, un one-liner cURL, la gestion des erreurs et des conseils pour les fichiers de plus de 10 Mo.
Si vous avez déjà eu besoin de pousser quelques milliers de contacts d’un tableur vers Brevo, vous savez que la voie manuelle de l’interface devient vite pénible : choisir le fichier, mapper les colonnes, choisir une liste, attendre, recommencer. Un script fait le même travail en quelques secondes et, plus important encore, vous pouvez le relancer de façon planifiée, après un export de base de données, ou chaque fois que votre CRM crache un nouveau CSV.
Ce guide couvre l’endpoint API que Brevo prévoit exactement pour ça, avec des scripts complets et fonctionnels en Python, Node.js et un one-liner cURL. Tout dans ce guide tape sur POST /v3/contacts/import.
L’endpoint en un coup d’œil
POST https://api.brevo.com/v3/contacts/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "listIds": [42], "updateExistingContacts": true, "emailBlacklist": false, "smsBlacklist": false}La réponse arrive vite :
{ "processId": 78 }C’est un 202 Accepted : Brevo a accepté l’import et le traite en arrière-plan. Le processId est votre poignée de suivi si vous voulez interroger l’état d’avancement ou configurer un webhook notifyUrl.
Quelques points à connaître avant d’écrire du code :
- Le corps peut être du CSV (
fileBody), du JSON (jsonBody) ou une URL distante (fileUrl). Choisissez-en un. La forme CSV utilise des points-virgules (;) pour séparer les colonnes, pas des virgules, c’est un piège classique. fileBodyetjsonBodysont plafonnés à 10 Mo. Brevo recommande de rester aux alentours de 8 Mo pour être tranquille. Pour quoi que ce soit de plus grand, uploadez le fichier sur S3, GCS ou n’importe quel hôte HTTPS et passezfileUrlà la place.- Les attributs personnalisés qui n’existent pas dans votre compte sont ignorés silencieusement. Créez-les dans l’interface Brevo (ou via l’API Attributes) avant d’importer des lignes qui les utilisent, sans quoi les données disparaissent simplement.
updateExistingContactsvauttruepar défaut. Si vous le mettez àfalse, Brevo ignore les contacts dont l’e-mail existe déjà, pratique pour les jobs « ajouter uniquement les nouveaux ».emptyContactsAttributesdétermine si les cellules vides de votre CSV effacent les valeurs existantes. Par défautfalse(les cellules vides sont ignorées). Passez àtruesi votre CSV est la source de vérité et que les blancs doivent effacer des données obsolètes.
Obtenir une clé API
Connectez-vous à Brevo → Paramètres → SMTP & API → API Keys → créez une nouvelle clé. Elle ressemble à xkeysib-.... Vous la passerez en en-tête HTTP api-key à chaque requête. Traitez-la comme un mot de passe, elle a un accès complet en lecture/écriture sur votre compte.
Bonne pratique : mettez-la dans une variable d’environnement pour qu’elle ne se retrouve jamais dans votre code source.
export BREVO_API_KEY="xkeysib-..."Python : lire un CSV, l’envoyer à Brevo
C’est le script le plus simple possible : lire un CSV local avec la bibliothèque standard, envoyer le corps directement à Brevo. Aucun SDK tiers nécessaire en dehors de 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}")Lancez-le :
python import_csv_to_brevo.py contacts.csvLa première ligne de votre CSV est l’en-tête. Les noms de colonne se mappent aux attributs Brevo par majuscules, correspondance exacte : EMAIL, FIRSTNAME, LASTNAME, plus tout attribut personnalisé que vous avez défini. EMAIL est obligatoire.
EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITYC’est tout le flux. Le script revient en moins d’une seconde, l’import réel tourne côté serveur et prend de quelques secondes à quelques minutes selon le volume.
Fichiers Excel : trois pistes viables
L’endpoint d’import de Brevo ne lit pas directement les .xlsx, vous avez donc trois vraies options selon l’endroit où le travail doit vivre :
- Lancer une macro VBA dans le classeur, synchronisation en un clic depuis Excel lui-même, sans script externe. C’est la bonne réponse quand le fichier vit sur un poste de travail et que votre équipe veut un bouton sur la feuille. Code complet dans le guide de la macro VBA Excel vers Brevo.
- Office Scripts + Power Automate, si le classeur est dans OneDrive/SharePoint et que vous voulez une synchronisation planifiée et sans surveillance. Également couvert dans le guide Excel.
- Convertir le
.xlsxen CSV depuis un script, ce que couvre le reste de cette section. C’est le mieux si vous faites déjà tourner Python ou Node sur un serveur et qu’il vous suffit d’aspirer un classeur une fois par jour.
Pour l’option 3, pandas le fait en une ligne :
# 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")Si vous ne voulez pas pandas comme dépendance, openpyxl seul fonctionne aussi :
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 : même travail, SDK officiel
Brevo publie @getbrevo/brevo pour Node. Il gère l’auth, les retries et la forme typée de la requête :
// 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}`);Ou, si vous ne voulez pas du SDK, un simple fetch fait pareil :
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 : un one-liner pour les imports ad hoc
Quand vous voulez juste tester l’endpoint ou pousser un petit fichier à la main :
# 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 . avale tout le fichier et l’échappe en JSON, ce qui vous évite d’échapper manuellement les sauts de ligne et les guillemets.
Fichiers de plus de 10 Mo : utilisez fileUrl
fileBody est plafonné à 10 Mo. Si votre dump de contacts est plus gros, hébergez le fichier à une URL que Brevo peut récupérer et passez-la :
body = { "fileUrl": "https://files.example.com/contacts/2026-04-30.csv", "listIds": [42], "updateExistingContacts": True,}Tout ce qui est accessible via une URL HTTPS publique fonctionne : URLs présignées S3, GCS, votre propre hôte statique, voire une URL raw GitHub pour des imports ponctuels. Brevo récupère le fichier depuis votre URL puis lance l’import. Formats acceptés : .csv, .txt, .json.
Envoyer du JSON plutôt que du CSV
Si vous tirez déjà les contacts d’une base de données, inutile de faire un aller-retour par CSV, envoyez-les directement en JSON :
body = { "jsonBody": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "attributes": { "FIRSTNAME": "John", "LASTNAME": "Smith", "COMPANY": "Globex", }, }, ], "listIds": [42],}Même plafond de 10 Mo. Même comportement asynchrone.
Sondage de l’état d’avancement
L’import est asynchrone, processId est votre poignée pour le suivre. Il y a un endpoint séparé pour vérifier l’état du processus :
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}")Pour les jobs longs, le motif plus propre est notifyUrl : passez un endpoint HTTPS auquel Brevo enverra un POST quand l’import sera terminé, et vous évitez le polling.
body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"Erreurs courantes et comment les corriger
400 Bad Request sans cause évidente. Presque toujours des points-virgules contre des virgules dans le CSV. L’import de Brevo attend ;, pas ,. Revérifiez fileBody après votre étape de conversion.
Les valeurs des attributs personnalisés disparaissent. L’attribut n’existe pas dans votre compte. Créez-le sous Contacts → Paramètres → Attributs de contact avant d’importer, ou utilisez l’API Attributes pour le créer dans le cadre de votre script.
401 Unauthorized. Mauvais nom d’en-tête. C’est api-key (en minuscules, avec tiret), pas Authorization ni X-API-Key.
Import « réussi » mais les contacts n’apparaissent pas dans la liste. Vérifiez que listIds contient bien la bonne liste. De plus : si updateExistingContacts est à false et que les contacts existent déjà, Brevo les ignore silencieusement plutôt que de les rajouter à la liste.
Certaines lignes ont été importées, d’autres non. Brevo vous envoie un rapport d’erreurs ligne par ligne par e-mail à la fin de l’import (sauf si vous avez mis disableNotification: true). Le rapport vous dit quelles lignes avaient des e-mails invalides, des champs requis manquants ou des problèmes de formatage.
Quand scripter et quand utiliser l’interface
L’interface convient pour les imports ponctuels de moins de mille lignes. Un script gagne dès que :
- Vous importez plus d’une fois (par ex. un export hebdomadaire de votre CRM)
- Les données source ont besoin d’un nettoyage avant d’arriver dans Brevo (déduplication, formatage des numéros de téléphone, séparation des noms complets en prénom/nom)
- Vous voulez que ça tourne de façon planifiée sans que personne ne clique
- Le fichier dépasse la zone de confort de l’interface
Enveloppez le script ci-dessus dans un cron ou une GitHub Action et vous avez une synchronisation automatique des contacts. Le prochain article de cette série montre comment faire la même chose directement depuis un Google Sheet via Apps Script, sans serveur.