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.

Featured image for article: Come importare contatti CSV o Excel in Brevo con uno script (Python, Node.js, cURL)

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/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 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.
  • fileBody e jsonBody sono 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 passa fileUrl invece.
  • 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 default true. Se lo metti su false, Brevo salta i contatti la cui email esiste già, utile per i job “aggiungi solo nuovi”.
  • emptyContactsAttributes controlla se le celle vuote nel tuo CSV cancellano i valori esistenti. Default false (le celle vuote vengono ignorate). Mettilo su true se 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.

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

Eseguilo:

Terminal window
python import_csv_to_brevo.py contacts.csv

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

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

  1. 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.
  2. 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.
  3. Convertire .xlsx in 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 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")

Se non vuoi pandas come dipendenza, anche openpyxl da solo funziona:

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: stesso lavoro, SDK ufficiale

Brevo pubblica @getbrevo/brevo per Node. Si occupa di auth, retry e della forma tipata della richiesta:

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

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:

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

Letture aggiuntive

Frequently Asked Questions

Qual è l'endpoint dell'API Brevo per l'importazione in massa dei contatti?
POST https://api.brevo.com/v3/contacts/import. Accetta contenuto CSV (fileBody), un URL di file remoto (fileUrl) o un array JSON (jsonBody). L'endpoint è asincrono, restituisce subito un processId e completa l'importazione in background.
Qual è la dimensione massima del file?
10 MB per CSV inline (fileBody) o JSON (jsonBody). Per file più grandi, ospita il file in un posto raggiungibile e passa il suo URL via fileUrl, quella strada non ha un limite di dimensione documentato.
Come importo un file .xlsx?
L'API di importazione di Brevo accetta solo .csv, .txt o .json. Converti prima il .xlsx in .csv, pandas, openpyxl o LibreOffice headless lo fanno in una riga. Lo script in questa guida include una conversione xlsx in csv.
Sovrascrive i contatti esistenti?
Di default sì, updateExistingContacts è impostato su true e fa il match sull'email. Mettilo su false per saltare i contatti che già esistono. Imposta emptyContactsAttributes su true se vuoi che le celle vuote del CSV cancellino i valori esistenti (altrimenti le celle vuote vengono ignorate).
Inizia gratis con Brevo