Cómo importar contactos CSV o Excel a Brevo con un script (Python, Node.js, cURL)
Importa contactos en bloque desde un archivo CSV o Excel a Brevo usando la API import-contacts. Incluye scripts listos para usar en Python y Node.js, un one-liner de cURL, manejo de errores y consejos para archivos de más de 10 MB.
Si alguna vez has tenido que mover unos miles de contactos desde una hoja de cálculo a Brevo, ya sabes que la ruta manual de la interfaz se vuelve pesada rápido: elegir el archivo, mapear columnas, elegir una lista, esperar, repetir. Un script hace lo mismo en segundos y, lo que es más importante, puedes volver a ejecutarlo de forma programada, después de un export de la base de datos o cada vez que tu CRM escupa un CSV nuevo.
Esta guía cubre el endpoint de la API que Brevo te da exactamente para esto, con scripts completos y funcionales en Python, Node.js y un one-liner de cURL. Todo en esta guía pega contra POST /v3/contacts/import.
El endpoint de un vistazo
POST https://api.brevo.com/v3/contacts/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "listIds": [42], "updateExistingContacts": true, "emailBlacklist": false, "smsBlacklist": false}La respuesta vuelve rápido:
{ "processId": 78 }Eso es un 202 Accepted: Brevo aceptó la importación y la está procesando en segundo plano. El processId es tu manija para hacer seguimiento si quieres consultar el estado o configurar un webhook con notifyUrl.
Algunos puntos que vale la pena conocer antes de escribir código:
- El cuerpo puede ser CSV (
fileBody), JSON (jsonBody) o una URL remota (fileUrl). Elige uno. La forma CSV usa punto y coma (;) para separar columnas, no comas, esa es una trampa habitual. fileBodyyjsonBodyestán limitados a 10 MB. Brevo recomienda quedarse alrededor de 8 MB para ir sobre seguro. Para algo más grande, sube el archivo a S3, GCS o cualquier host HTTPS y pasafileUrlen su lugar.- Los atributos personalizados que no existen en tu cuenta se ignoran en silencio. Créalos en la interfaz de Brevo (o vía la API de Atributos) antes de importar filas que los usen, de lo contrario los datos simplemente desaparecen.
updateExistingContactspor defecto estrue. Si lo pones enfalse, Brevo se salta los contactos cuyo email ya existe, útil para tareas de “solo añadir nuevos”.emptyContactsAttributescontrola si las celdas vacías del CSV borran los valores existentes. Por defectofalse(las celdas vacías se ignoran). Ponlo entruesi tu CSV es la fuente de verdad y quieres que los blancos borren los datos obsoletos.
Conseguir una API key
Inicia sesión en Brevo → Configuración → SMTP & API → API Keys → crea una nueva clave. Tendrá la forma xkeysib-.... La pasarás como header HTTP api-key en cada petición. Trátala como una contraseña, tiene acceso de lectura/escritura completo a tu cuenta.
Un patrón limpio: ponla en una variable de entorno para que nunca acabe en tu árbol de fuentes.
export BREVO_API_KEY="xkeysib-..."Python: leer un CSV y enviarlo a Brevo
Este es el script más simple posible: leer un CSV local con la biblioteca estándar y enviar el cuerpo directamente a Brevo. No necesitas ningún SDK de terceros aparte 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}")Ejecútalo:
python import_csv_to_brevo.py contacts.csvLa primera fila de tu CSV es la cabecera. Los nombres de columna se mapean a los atributos de Brevo por mayúsculas, coincidencia exacta: EMAIL, FIRSTNAME, LASTNAME, además de cualquier atributo personalizado que hayas definido. EMAIL es obligatorio.
EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITYEse es todo el flujo. El script vuelve en menos de un segundo; la importación real corre en el servidor y tarda entre unos pocos segundos y unos minutos según el volumen.
Archivos Excel: tres caminos viables
El endpoint de importación de Brevo no lee .xlsx directamente, así que tienes tres opciones reales según dónde necesites que viva el trabajo:
- Ejecutar una macro VBA dentro del libro, sincronización de un clic desde el propio Excel, sin script externo. Es la respuesta correcta cuando el archivo vive en un escritorio y tu equipo quiere un botón en la hoja. Código completo en la guía de macro VBA de Excel a Brevo.
- Office Scripts + Power Automate, si el libro vive en OneDrive/SharePoint y quieres sincronización programada y desatendida. También se cubre en la guía de Excel.
- Convertir
.xlsxa CSV desde un script, lo que cubre el resto de esta sección. Es la mejor opción si ya tienes Python o Node corriendo en un servidor y solo necesitas tirar de un libro una vez al día.
Para la opción 3, pandas lo hace en una línea:
# 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 no quieres pandas como dependencia, openpyxl solo también funciona:
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: el mismo trabajo, SDK oficial
Brevo publica @getbrevo/brevo para Node. Maneja la autenticación, los reintentos y la forma tipada de la petición:
// 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, si no quieres el SDK, fetch plano funciona igual:
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 para importaciones puntuales
Cuando solo quieres probar el endpoint o meter un archivo pequeño a mano:
# 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 . se traga el archivo entero y lo escapa como JSON, te ahorra escapar manualmente saltos de línea y comillas.
Archivos de más de 10 MB: usa fileUrl
fileBody está limitado a 10 MB. Si tu volcado de contactos es mayor, aloja el archivo en una URL que Brevo pueda descargar y pásala:
body = { "fileUrl": "https://files.example.com/contacts/2026-04-30.csv", "listIds": [42], "updateExistingContacts": True,}Cualquier cosa accesible vía una URL HTTPS pública funciona: URLs prefirmadas de S3, GCS, tu propio host estático, incluso una URL raw de GitHub para importaciones puntuales. Brevo descarga el archivo desde tu URL y luego ejecuta la importación. Formatos aceptados: .csv, .txt, .json.
Enviar JSON en lugar de CSV
Si ya estás sacando contactos de una base de datos, no necesitas dar la vuelta por CSV: envíalos directamente como JSON:
body = { "jsonBody": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "attributes": { "FIRSTNAME": "John", "LASTNAME": "Smith", "COMPANY": "Globex", }, }, ], "listIds": [42],}Mismo límite de 10 MB. Mismo comportamiento asíncrono.
Sondeo del estado
La importación es asíncrona, processId es tu manija para seguirla. Hay un endpoint separado para consultar el estado del proceso:
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}")Para trabajos largos, el patrón más limpio es notifyUrl: pasa un endpoint HTTPS al que Brevo hará POST cuando termine la importación, y ahórrate el sondeo.
body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"Errores comunes y cómo solucionarlos
400 Bad Request sin causa obvia. Casi siempre es punto y coma frente a comas en el CSV. La importación de Brevo espera ;, no ,. Vuelve a comprobar fileBody después de tu paso de conversión.
Los valores de atributos personalizados desaparecen. El atributo no existe en tu cuenta. Créalo en Contactos → Configuración → Atributos de contacto antes de importar, o usa la API de Atributos para crearlo como parte de tu script.
401 Unauthorized. Nombre de header equivocado. Es api-key (en minúsculas, con guion), no Authorization ni X-API-Key.
La importación dice “succeeded” pero los contactos no aparecen en la lista. Comprueba que listIds contiene la lista correcta. Además: si updateExistingContacts es false y los contactos ya existen, Brevo los salta en silencio en lugar de volver a añadirlos a la lista.
Algunas filas se importaron, otras no. Brevo te envía por email un informe de errores fila a fila después de que termine la importación (a no ser que pongas disableNotification: true). El informe te dice qué filas tenían emails malos, faltaban campos obligatorios o tenían problemas de formato.
Cuándo hacer un script y cuándo usar la interfaz
La interfaz está bien para importaciones puntuales por debajo de mil filas. Un script gana en cuanto:
- Vas a importar más de una vez (por ejemplo, un export semanal de tu CRM)
- Los datos de origen necesitan limpieza antes de aterrizar en Brevo (deduplicación, formateo de números de teléfono, separar nombre y apellido)
- Quieres que corra de forma programada sin que nadie haga clic
- El archivo es más grande de lo que la interfaz maneja con comodidad
Envuelve el script de arriba en un cron job o una GitHub Action y tienes sincronización automática de contactos. El siguiente post de esta serie muestra cómo hacer lo mismo directamente desde un Google Sheet usando Apps Script, sin servidor.