Como importar contatos CSV ou Excel para a Brevo com um script (Python, Node.js, cURL)

Importe contatos em massa de um arquivo CSV ou Excel para a Brevo usando a API import-contacts. Inclui scripts Python e Node.js prontos para rodar, um one-liner cURL, tratamento de erros e dicas para arquivos maiores que 10 MB.

Featured image for article: Como importar contatos CSV ou Excel para a Brevo com um script (Python, Node.js, cURL)

Se você já precisou empurrar uns milhares de contatos de uma planilha para a Brevo, sabe que o caminho manual da interface fica chato rápido: escolher o arquivo, mapear colunas, escolher uma lista, esperar, repetir. Um script faz o mesmo trabalho em segundos e, mais importante ainda, você pode rodar de novo de forma agendada, depois de um export do banco de dados ou sempre que o seu CRM cuspir um CSV novo.

Esta guia cobre o endpoint da API que a Brevo te dá exatamente para isso, com scripts completos e funcionando em Python, Node.js e um one-liner cURL. Tudo nesta guia bate em POST /v3/contacts/import.

O endpoint num relance

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
}

A resposta volta rápido:

{ "processId": 78 }

Isso é um 202 Accepted, a Brevo aceitou a importação e está processando em segundo plano. O processId é o seu identificador de rastreamento se quiser fazer polling até terminar ou configurar um webhook notifyUrl.

Algumas coisas que vale a pena saber antes de escrever código:

  • O corpo pode ser CSV (fileBody), JSON (jsonBody) ou uma URL remota (fileUrl). Escolha um. A forma CSV usa ponto e vírgula (;) para separar colunas, não vírgula, é uma pegadinha clássica.
  • fileBody e jsonBody têm teto de 10 MB. A Brevo recomenda ficar em torno de 8 MB para ir tranquilo. Para qualquer coisa maior, suba o arquivo para S3, GCS ou qualquer host HTTPS e passe fileUrl no lugar.
  • Atributos personalizados que não existem na sua conta são ignorados em silêncio. Crie eles na interface da Brevo (ou via API de Atributos) antes de importar linhas que os usem, do contrário os dados simplesmente somem.
  • updateExistingContacts é true por padrão. Se você colocar como false, a Brevo pula contatos cujo email já existe, útil para tarefas de “adicionar só os novos”.
  • emptyContactsAttributes controla se células vazias do seu CSV apagam valores existentes. Padrão false (células vazias são ignoradas). Coloque como true se o seu CSV é a fonte da verdade e os vazios devem limpar dados desatualizados.

Conseguir uma API key

Faça login na Brevo → Configurações → SMTP & API → API Keys → crie uma nova chave. Tem o formato xkeysib-.... Você vai passar como header HTTP api-key em cada requisição. Trate como uma senha, ela tem acesso completo de leitura e escrita na sua conta.

Padrão limpo: coloque numa variável de ambiente para nunca cair na sua árvore de fontes.

Terminal window
export BREVO_API_KEY="xkeysib-..."

Python: ler um CSV, enviar para a Brevo

Este é o script mais simples possível: ler um CSV local com a biblioteca padrão, enviar o corpo direto para a Brevo. Não precisa de nenhum SDK de terceiros além do 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}")

Rode:

Terminal window
python import_csv_to_brevo.py contacts.csv

A primeira linha do seu CSV é o cabeçalho. Os nomes das colunas são mapeados para atributos da Brevo por maiúsculas, correspondência exata: EMAIL, FIRSTNAME, LASTNAME, mais qualquer atributo personalizado que você tenha definido. EMAIL é obrigatório.

EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITY
[email protected],Jane,Doe,Acme,Berlin
[email protected],John,Smith,Globex,Paris

Esse é o fluxo todo. O script volta em menos de um segundo, a importação real roda no servidor e leva entre poucos segundos e alguns minutos dependendo do volume.

Arquivos Excel: três caminhos viáveis

O endpoint de importação da Brevo não lê .xlsx diretamente, então você tem três opções reais dependendo de onde o trabalho precisa viver:

  1. Rodar uma macro VBA dentro da pasta de trabalho, sincronização em um clique do próprio Excel, sem script externo. É a resposta certa quando o arquivo vive num desktop e seu time quer um botão na planilha. Código completo no guia da macro VBA do Excel para a Brevo.
  2. Office Scripts + Power Automate, se a pasta de trabalho está no OneDrive/SharePoint e você quer sincronização agendada e desassistida. Também coberto no guia do Excel.
  3. Converter .xlsx para CSV via script, o que cobre o resto desta seção. Melhor se você já está rodando Python ou Node num servidor e só precisa puxar uma pasta de trabalho uma vez por dia.

Para a opção 3, pandas faz isso em uma linha:

# 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 você não quer pandas como dependência, openpyxl sozinho também 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: mesmo trabalho, SDK oficial

A Brevo publica @getbrevo/brevo para Node. Ele cuida da autenticação, retries e a forma tipada da requisição:

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, se você não quer o SDK, um fetch puro funciona da mesma forma:

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: um one-liner para importações pontuais

Quando você só quer testar o endpoint ou enfiar um arquivo pequeno na mão:

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 . engole o arquivo inteiro e faz escape como JSON, evita ter que escapar manualmente quebras de linha e aspas.

Arquivos maiores que 10 MB: use fileUrl

fileBody tem teto de 10 MB. Se o seu dump de contatos é maior, hospede o arquivo numa URL que a Brevo possa baixar e passe ela:

body = {
"fileUrl": "https://files.example.com/contacts/2026-04-30.csv",
"listIds": [42],
"updateExistingContacts": True,
}

Qualquer coisa acessível por uma URL HTTPS pública funciona, URLs pré-assinadas do S3, GCS, seu próprio host estático, até uma URL raw do GitHub para importações pontuais. A Brevo busca o arquivo na sua URL e depois roda a importação. Formatos aceitos: .csv, .txt, .json.

Enviar JSON em vez de CSV

Se você já está puxando contatos de um banco de dados, não precisa fazer o desvio pelo CSV, mande direto 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],
}

Mesmo teto de 10 MB. Mesmo comportamento assíncrono.

Polling até terminar

A importação é assíncrona, processId é o seu identificador para rastrear. Existe um endpoint separado para checar o status do 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}")

Para tarefas longas, o padrão mais limpo é notifyUrl: passe um endpoint HTTPS para o qual a Brevo vai fazer POST quando a importação terminar, e dispense o polling.

body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"

Erros comuns e como resolver

400 Bad Request sem causa óbvia. Quase sempre é ponto e vírgula contra vírgula no CSV. A importação da Brevo espera ;, não ,. Confira de novo o fileBody depois do seu passo de conversão.

Valores de atributos personalizados somem. O atributo não existe na sua conta. Crie ele em Contatos → Configurações → Atributos do contato antes de importar, ou use a API de Atributos para criar como parte do seu script.

401 Unauthorized. Nome de header errado. É api-key (minúsculo, com hífen), não Authorization nem X-API-Key.

Importação “succeeded” mas os contatos não apareceram na lista. Confira se listIds contém a lista certa. Além disso: se updateExistingContacts está false e os contatos já existem, a Brevo os pula em silêncio em vez de adicionar de novo na lista.

Algumas linhas importadas, outras não. A Brevo te manda um relatório de erros linha por linha por email depois que a importação termina (a não ser que você coloque disableNotification: true). O relatório te diz quais linhas tinham email inválido, campos obrigatórios faltando ou problemas de formatação.

Quando script e quando interface

A interface está bem para importações pontuais com menos de mil linhas. Um script ganha assim que:

  • Você importa mais de uma vez (por exemplo, export semanal do seu CRM)
  • Os dados de origem precisam ser limpos antes de cair na Brevo (deduplicação, formatar números de telefone, separar nomes completos em primeiro e último)
  • Você quer que rode de forma agendada sem ninguém clicando
  • O arquivo é maior do que a zona de conforto da interface

Embrulhe o script acima num cron job ou numa GitHub Action e você tem sincronização automática de contatos. O próximo post desta série mostra como fazer a mesma coisa direto de um Google Sheet usando Apps Script, sem servidor.

Leitura adicional

Frequently Asked Questions

Qual é o endpoint da API da Brevo para importação de contatos em massa?
POST https://api.brevo.com/v3/contacts/import. Aceita conteúdo CSV (fileBody), uma URL de arquivo remoto (fileUrl) ou um array JSON (jsonBody). O endpoint é assíncrono, devolve um processId na hora e termina a importação em segundo plano.
Qual é o tamanho máximo de arquivo?
10 MB para CSV inline (fileBody) ou JSON (jsonBody). Para arquivos maiores, hospede o arquivo em um lugar acessível e passe a URL via fileUrl, esse caminho não tem limite de tamanho documentado.
Como importo um arquivo .xlsx?
A API de importação da Brevo aceita apenas .csv, .txt ou .json. Converta primeiro o .xlsx para .csv, pandas, openpyxl ou LibreOffice headless fazem isso em uma linha. O script desta guia inclui uma conversão de xlsx para csv.
Vai sobrescrever os contatos existentes?
Por padrão, sim: updateExistingContacts está como true e faz o match pelo email. Coloque como false para pular contatos que já existem. Coloque emptyContactsAttributes como true se quiser que células vazias do CSV apaguem valores existentes (caso contrário, células vazias são ignoradas).
Comece grátis com Brevo