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.

Featured image for article: Cómo importar contactos CSV o Excel a Brevo con un script (Python, Node.js, cURL)

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/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 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.
  • fileBody y jsonBody está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 pasa fileUrl en 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.
  • updateExistingContacts por defecto es true. Si lo pones en false, Brevo se salta los contactos cuyo email ya existe, útil para tareas de “solo añadir nuevos”.
  • emptyContactsAttributes controla si las celdas vacías del CSV borran los valores existentes. Por defecto false (las celdas vacías se ignoran). Ponlo en true si 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.

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

Ejecútalo:

Terminal window
python import_csv_to_brevo.py contacts.csv

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

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

  1. 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.
  2. 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.
  3. Convertir .xlsx a 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 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 no quieres pandas como dependencia, openpyxl solo también funciona:

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

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

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

Lecturas adicionales

Frequently Asked Questions

¿Cuál es el endpoint de la API de Brevo para importar contactos en bloque?
POST https://api.brevo.com/v3/contacts/import. Acepta contenido CSV (fileBody), una URL de archivo remoto (fileUrl) o un array JSON (jsonBody). El endpoint es asíncrono: devuelve un processId al instante y termina la importación en segundo plano.
¿Cuál es el tamaño máximo del archivo?
10 MB para CSV en línea (fileBody) o JSON (jsonBody). Para archivos más grandes, alójalo en un sitio accesible y pasa su URL mediante fileUrl, esa vía no tiene un límite de tamaño documentado.
¿Cómo importo un archivo .xlsx?
La API de importación de Brevo solo acepta .csv, .txt o .json. Convierte primero el .xlsx a .csv: pandas, openpyxl o LibreOffice headless lo hacen en una línea. El script de esta guía incluye una conversión xlsx a csv.
¿Sobrescribirá los contactos existentes?
Por defecto, sí: updateExistingContacts está en true y hace match por email. Ponlo en false para saltarte los contactos que ya existen. Pon emptyContactsAttributes en true si quieres que las celdas vacías del CSV borren los valores existentes (de lo contrario, las celdas vacías se ignoran).
Empieza gratis con Brevo