Come importare contatti CSV o Excel in Brevo con uno script (Python, Node.js, cURL)
Importa in massa i contatti da un file CSV o Excel in Brevo usando l'API import-contacts. Include script Python e Node.js pronti all'uso, un one-liner cURL, gestione degli errori e consigli per file più grandi di 10 MB.
Se hai mai dovuto spingere qualche migliaio di contatti da un foglio di calcolo in Brevo, sai che la strada manuale dell’interfaccia diventa pesante in fretta: scegli il file, mappa le colonne, scegli una lista, aspetti, ripeti. Uno script fa lo stesso lavoro in pochi secondi e (cosa più importante) puoi rilanciarlo in modo programmato, dopo un export del database o ogni volta che il tuo CRM sputa fuori un nuovo CSV.
Questa guida copre l’endpoint dell’API che Brevo ti dà esattamente per questo, con script completi e funzionanti in Python, Node.js e un one-liner cURL. Tutto in questa guida colpisce POST /v3/contacts/import.
L’endpoint a colpo d’occhio
POST https://api.brevo.com/v3/contacts/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "listIds": [42], "updateExistingContacts": true, "emailBlacklist": false, "smsBlacklist": false}La risposta torna in fretta:
{ "processId": 78 }Quello è un 202 Accepted, Brevo ha accettato l’importazione e la sta processando in background. Il processId è la tua maniglia di tracking se vuoi fare polling per il completamento o configurare un webhook notifyUrl.
Qualche punto da conoscere prima di scrivere codice:
- Il body può essere CSV (
fileBody), JSON (jsonBody) o un URL remoto (fileUrl). Sceglierne uno. La forma CSV usa il punto e virgola (;) per separare le colonne, non la virgola, è una trappola classica. fileBodyejsonBodysono limitati a 10 MB. Brevo consiglia di stare attorno agli 8 MB per andare sul sicuro. Per qualsiasi cosa più grande, carica il file su S3, GCS o un qualsiasi host HTTPS e passafileUrlinvece.- Gli attributi personalizzati che non esistono nel tuo account vengono ignorati silenziosamente. Creali nell’interfaccia di Brevo (o tramite l’API Attributes) prima di importare righe che li usano, altrimenti i dati spariscono e basta.
updateExistingContactsè di defaulttrue. Se lo metti sufalse, Brevo salta i contatti la cui email esiste già, utile per i job “aggiungi solo nuovi”.emptyContactsAttributescontrolla se le celle vuote nel tuo CSV cancellano i valori esistenti. Defaultfalse(le celle vuote vengono ignorate). Mettilo sutruese il tuo CSV è la fonte di verità e vuoi che i vuoti puliscano i dati obsoleti.
Ottenere una API key
Accedi a Brevo → Impostazioni → SMTP & API → API Keys → crea una nuova chiave. Ha la forma xkeysib-.... La passerai come header HTTP api-key a ogni richiesta. Trattala come una password, ha accesso completo in lettura e scrittura sul tuo account.
Pratica pulita: mettila in una variable d’ambiente così non finisce mai nel tuo source tree.
export BREVO_API_KEY="xkeysib-..."Python: leggere un CSV, mandarlo a Brevo
Questo è lo script più semplice possibile: leggere un CSV locale con la libreria standard, mandare il body direttamente a Brevo. Non serve nessun SDK di terze parti oltre a 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}")Eseguilo:
python import_csv_to_brevo.py contacts.csvLa prima riga del tuo CSV è l’header. I nomi di colonna vengono mappati agli attributi Brevo per maiuscolo, corrispondenza esatta: EMAIL, FIRSTNAME, LASTNAME, oltre a qualsiasi attributo personalizzato che hai definito. EMAIL è obbligatorio.
EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITYQuello è tutto il flusso. Lo script ritorna entro un secondo, l’importazione vera gira lato server e impiega da pochi secondi a qualche minuto a seconda del volume.
File Excel: tre strade percorribili
L’endpoint di importazione di Brevo non legge .xlsx direttamente, quindi hai tre opzioni reali a seconda di dove deve vivere il lavoro:
- Lanciare una macro VBA dentro la cartella di lavoro, sincronizzazione in un clic da Excel stesso, senza script esterno. È la risposta giusta quando il file vive su un desktop e il tuo team vuole un pulsante sul foglio. Codice completo nella guida alla macro VBA da Excel a Brevo.
- Office Scripts + Power Automate, se la cartella di lavoro è in OneDrive/SharePoint e vuoi sincronizzazione programmata e non presidiata. Coperto anche nella guida a Excel.
- Convertire
.xlsxin CSV da uno script, ciò che copre il resto di questa sezione. Migliore se stai già facendo girare Python o Node su un server e ti serve solo tirare dentro una cartella di lavoro una volta al giorno.
Per l’opzione 3, pandas lo fa in una riga:
# 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")Se non vuoi pandas come dipendenza, anche openpyxl da solo funziona:
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: stesso lavoro, SDK ufficiale
Brevo pubblica @getbrevo/brevo per Node. Si occupa di auth, retry e della forma tipata della richiesta:
// 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}`);O, se non vuoi l’SDK, un semplice fetch funziona allo stesso modo:
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 per importazioni ad hoc
Quando vuoi solo testare l’endpoint o spingere a mano un piccolo file:
# 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 . ingoia tutto il file e lo escapa come JSON, ti risparmia di fare l’escape manuale di newline e virgolette.
File più grandi di 10 MB: usa fileUrl
fileBody è limitato a 10 MB. Se il tuo dump di contatti è più grande, ospita il file a un URL che Brevo possa scaricare e passa quello:
body = { "fileUrl": "https://files.example.com/contacts/2026-04-30.csv", "listIds": [42], "updateExistingContacts": True,}Funziona qualsiasi cosa raggiungibile via un URL HTTPS pubblico, URL prefirmati S3, GCS, il tuo host statico, anche un raw URL GitHub per importazioni una tantum. Brevo scarica il file dal tuo URL, poi lancia l’importazione. Formati accettati: .csv, .txt, .json.
Inviare JSON invece di CSV
Se stai già tirando i contatti da un database, non serve fare il giro tramite CSV, mandali direttamente come JSON:
body = { "jsonBody": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "attributes": { "FIRSTNAME": "John", "LASTNAME": "Smith", "COMPANY": "Globex", }, }, ], "listIds": [42],}Stesso limite di 10 MB. Stesso comportamento asincrono.
Polling per il completamento
L’importazione è asincrona, processId è la tua maniglia per seguirla. C’è un endpoint separato per controllare lo stato del processo:
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}")Per i job lunghi, il pattern più pulito è notifyUrl: passa un endpoint HTTPS al quale Brevo farà POST quando l’importazione finisce, e risparmiati il polling.
body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"Errori comuni e come risolverli
400 Bad Request senza causa ovvia. Quasi sempre punto e virgola contro virgole nel CSV. L’importazione di Brevo si aspetta ;, non ,. Ricontrolla fileBody dopo il tuo passo di conversione.
I valori degli attributi personalizzati spariscono. L’attributo non esiste nel tuo account. Crealo in Contatti → Impostazioni → Attributi del contatto prima di importare, oppure usa l’API Attributes per crearlo come parte del tuo script.
401 Unauthorized. Nome dell’header sbagliato. È api-key (minuscolo, con trattino), non Authorization né X-API-Key.
Importazione “andata bene” ma i contatti non compaiono nella lista. Controlla che listIds contenga la lista giusta. Inoltre: se updateExistingContacts è false e i contatti già esistono, Brevo li salta in silenzio invece di riaggiungerli alla lista.
Alcune righe importate, altre no. Brevo ti manda un report errori riga per riga via email dopo che l’importazione è finita (a meno che tu non imposti disableNotification: true). Il report ti dice quali righe avevano email errate, campi obbligatori mancanti o problemi di formattazione.
Quando scriptare e quando usare l’interfaccia
L’interfaccia va bene per importazioni una tantum sotto le mille righe. Uno script vince non appena:
- Importi più di una volta (per esempio, export settimanale dal tuo CRM)
- I dati di origine hanno bisogno di pulizia prima di atterrare in Brevo (deduplicazione, formattazione dei numeri di telefono, suddivisione dei nomi completi in nome e cognome)
- Vuoi che giri in modo programmato senza che nessuno clicchi
- Il file è più grande della zona di comfort dell’interfaccia
Avvolgi lo script qui sopra in un cron job o in una GitHub Action e hai sincronizzazione automatica dei contatti. Il prossimo post di questa serie mostra come fare la stessa cosa direttamente da un Google Sheet usando Apps Script, senza server.