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.

Featured image for article: Comment importer des contacts CSV ou Excel dans Brevo avec un script (Python, Node.js, cURL)

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/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
}

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.
  • fileBody et jsonBody sont 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 passez fileUrl à 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.
  • updateExistingContacts vaut true par 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 ».
  • emptyContactsAttributes détermine si les cellules vides de votre CSV effacent les valeurs existantes. Par défaut false (les cellules vides sont ignorées). Passez à true si 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.

Terminal window
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_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}")

Lancez-le :

Terminal window
python import_csv_to_brevo.py contacts.csv

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

C’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 :

  1. 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.
  2. 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.
  3. Convertir le .xlsx en 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 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")

Si vous ne voulez pas pandas comme dépendance, openpyxl seul fonctionne aussi :

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

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}`);

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 :

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 . 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": [
{
"email": "[email protected]",
"attributes": {
"FIRSTNAME": "Jane",
"LASTNAME": "Doe",
"COMPANY": "Acme",
},
},
{
"email": "[email protected]",
"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.

Pour aller plus loin

Frequently Asked Questions

Quel est l'endpoint de l'API Brevo pour l'import de contacts en masse ?
POST https://api.brevo.com/v3/contacts/import. Il accepte du contenu CSV (fileBody), une URL de fichier distant (fileUrl) ou un tableau JSON (jsonBody). L'endpoint est asynchrone, il renvoie immédiatement un processId et termine l'import en arrière-plan.
Quelle est la taille maximale du fichier ?
10 Mo pour un CSV en ligne (fileBody) ou un JSON (jsonBody). Pour les fichiers plus gros, hébergez le fichier à un endroit accessible et passez son URL via fileUrl, cette voie n'a pas de limite de taille documentée.
Comment importer un fichier .xlsx ?
L'API d'import de Brevo n'accepte que .csv, .txt ou .json. Convertissez d'abord le .xlsx en .csv : pandas, openpyxl ou LibreOffice headless le font en une ligne. Le script de ce guide inclut une conversion xlsx vers csv.
Est-ce que cela écrasera les contacts existants ?
Par défaut, oui : updateExistingContacts est à true et fait la correspondance par e-mail. Mettez-le à false pour ignorer les contacts qui existent déjà. Mettez emptyContactsAttributes à true si vous voulez que les cellules vides du CSV effacent les valeurs existantes (sinon, les cellules vides sont ignorées).
Commencez gratuitement avec Brevo