Jak zaimportować kontakty CSV lub Excel do Brevo za pomocą skryptu (Python, Node.js, cURL)

Masowy import kontaktów z pliku CSV lub Excel do Brevo przy użyciu API import-contacts. Zawiera gotowe do uruchomienia skrypty w Pythonie i Node.js, jednolinijkowy cURL, obsługę błędów oraz wskazówki dla plików większych niż 10 MB.

Featured image for article: Jak zaimportować kontakty CSV lub Excel do Brevo za pomocą skryptu (Python, Node.js, cURL)

Jeśli kiedykolwiek musiałeś przesłać kilka tysięcy kontaktów z arkusza kalkulacyjnego do Brevo, ręczna ścieżka UI szybko staje się męcząca: wybierz plik, zmapuj kolumny, wybierz listę, czekaj, powtórz. Skrypt wykonuje tę samą pracę w sekundach, a co ważniejsze, możesz uruchomić go ponownie według harmonogramu, po eksporcie z bazy danych lub gdy Twój CRM wypluje świeży CSV.

Ten przewodnik omawia endpoint API, który Brevo oferuje dokładnie do tego celu, wraz z kompletnymi działającymi skryptami w Pythonie, Node.js oraz jednolinijkowym cURL. Wszystko w tym przewodniku trafia w POST /v3/contacts/import.

Endpoint w skrócie

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
}

Odpowiedź wraca szybko:

{ "processId": 78 }

To 202 Accepted. Brevo zaakceptował import i przetwarza go w tle. processId to Twój uchwyt do śledzenia, jeśli chcesz pollować status zakończenia lub skonfigurować webhook notifyUrl.

Kilka rzeczy wartych uwagi przed napisaniem kodu:

  • Treść może być CSV (fileBody), JSON (jsonBody) lub zdalnym URL (fileUrl). Wybierz jedno. Forma CSV używa średników (;) do oddzielania kolumn, a nie przecinków. To częsta pułapka.
  • fileBody i jsonBody są ograniczone do 10 MB. Brevo zaleca pozostanie w okolicach 8 MB dla bezpieczeństwa. Dla większych plików prześlij plik na S3, GCS lub dowolny host HTTPS i przekaż fileUrl.
  • Niestandardowe atrybuty, które nie istnieją w Twoim koncie, są po cichu ignorowane. Utwórz je w UI Brevo (lub przez API atrybutów) przed importem wierszy, które ich używają, w przeciwnym razie dane po prostu znikną.
  • updateExistingContacts ma domyślnie wartość true. Jeśli ustawisz na false, Brevo pomija kontakty, których e-mail już istnieje. Przydatne dla zadań typu “tylko nowe”.
  • emptyContactsAttributes kontroluje, czy puste komórki w CSV wymazują istniejące wartości. Domyślnie false (puste komórki są ignorowane). Ustaw na true, jeśli Twój CSV jest źródłem prawdy i chcesz, by puste pola czyściły nieaktualne dane.

Pobierz klucz API

Zaloguj się do Brevo, przejdź do Settings → SMTP & API → API Keys i utwórz nowy klucz. Wygląda jak xkeysib-.... Przekażesz go jako nagłówek HTTP api-key przy każdym żądaniu. Traktuj go jak hasło, ma pełne prawa odczytu/zapisu na Twoim koncie.

Czysty wzorzec: umieść go w zmiennej środowiskowej, by nigdy nie trafił do drzewa źródłowego.

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

Python: odczyt CSV i wysyłka do Brevo

To najprostszy możliwy skrypt: odczytaj lokalny CSV ze standardowej biblioteki, wyślij body bezpośrednio do Brevo. Nie potrzeba żadnego SDK firm trzecich poza 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}")

Uruchom go:

Terminal window
python import_csv_to_brevo.py contacts.csv

Pierwszy wiersz Twojego CSV to nagłówek. Nazwy kolumn mapują się do atrybutów Brevo poprzez dokładne dopasowanie wielkimi literami: EMAIL, FIRSTNAME, LASTNAME plus dowolny niestandardowy atrybut, który zdefiniowałeś. EMAIL jest obowiązkowy.

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

To cały przepływ. Skrypt wraca w ciągu sekundy; faktyczny import działa po stronie serwera i zajmuje od kilku sekund do kilku minut, w zależności od wolumenu.

Pliki Excel: trzy realne ścieżki

Endpoint importu Brevo nie czyta .xlsx bezpośrednio, więc masz trzy realne opcje w zależności od tego, gdzie ma się odbywać praca:

  1. Uruchom makro VBA wewnątrz skoroszytu. Synchronizacja jednym kliknięciem z samego Excela, bez zewnętrznego skryptu. To właściwa odpowiedź, gdy plik znajduje się na pulpicie i Twój zespół chce mieć przycisk na arkuszu. Pełny kod w przewodniku Excel-do-Brevo VBA.
  2. Office Scripts + Power Automate. Jeśli skoroszyt znajduje się w OneDrive/SharePoint i chcesz nienadzorowanej zaplanowanej synchronizacji. Również omówione w przewodniku Excel.
  3. Konwertuj .xlsx na CSV ze skryptu. Co obejmuje reszta tej sekcji. Najlepsze, jeśli już uruchamiasz Pythona lub Node na serwerze i potrzebujesz pobrać skoroszyt raz dziennie.

Dla opcji 3 pandas robi z tego jednolinijkowca:

# 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")

Jeśli nie chcesz pandas jako zależności, sam openpyxl też działa:

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: ta sama praca, oficjalny SDK

Brevo publikuje @getbrevo/brevo dla Node. Obsługuje uwierzytelnianie, ponawianie i typowany kształt żądania:

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

Lub, jeśli nie chcesz SDK, zwykły fetch działa tak samo:

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: jednolinijkowiec dla doraźnych importów

Gdy chcesz po prostu przetestować endpoint lub ręcznie wepchnąć mały plik:

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 . wciąga cały plik i JSON-escape’uje go, oszczędzając Ci ręcznego escape’owania nowych linii i cudzysłowów.

Pliki większe niż 10 MB: użyj fileUrl

fileBody jest ograniczony do 10 MB. Jeśli Twój zrzut kontaktów jest większy, hostuj plik pod adresem URL, który Brevo może pobrać, i przekaż go:

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

Wszystko, co jest osiągalne przez publiczny adres URL HTTPS, działa: pre-podpisane URL-e S3, GCS, Twój własny statyczny host, nawet surowy URL GitHub dla jednorazowych importów. Brevo pobiera plik z Twojego URL, a następnie uruchamia import. Zaakceptowane formaty: .csv, .txt, .json.

Wyślij JSON zamiast CSV

Jeśli już wyciągasz kontakty z bazy danych, nie musisz przechodzić przez CSV. Wyślij je bezpośrednio jako 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],
}

Ten sam limit 10 MB. To samo asynchroniczne zachowanie.

Polling do zakończenia

Import jest asynchroniczny. processId to Twój uchwyt do śledzenia. Istnieje osobny endpoint do sprawdzania statusu procesu:

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}")

Dla długo działających zadań czystszym wzorcem jest notifyUrl: przekaż endpoint HTTPS, do którego Brevo wyśle POST po zakończeniu importu, i pomiń polling.

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

Częste błędy i jak je naprawić

400 Bad Request bez oczywistej przyczyny. Prawie zawsze chodzi o średniki kontra przecinki w CSV. Import Brevo oczekuje ;, a nie ,. Sprawdź dwukrotnie fileBody po kroku konwersji.

Wartości niestandardowych atrybutów znikają. Atrybut nie istnieje w Twoim koncie. Utwórz go w Contacts → Settings → Contact attributes przed importem lub użyj API atrybutów, aby utworzyć go jako część swojego skryptu.

401 Unauthorized. Zła nazwa nagłówka. To api-key (małe litery, łącznik), a nie Authorization ani X-API-Key.

Import “powiódł się”, ale kontakty nie pojawiły się na liście. Sprawdź, czy listIds zawiera właściwą listę. Również: jeśli updateExistingContacts jest false, a kontakty już istnieją, Brevo po cichu je pomija, zamiast dodawać ponownie do listy.

Niektóre wiersze zaimportowane, niektóre nie. Brevo wysyła Ci e-mail z raportem błędów dla poszczególnych wierszy po zakończeniu importu (chyba że ustawisz disableNotification: true). Raport mówi, które wiersze miały złe e-maile, brakujące wymagane pola lub problemy z formatowaniem.

Kiedy skryptować, a kiedy używać UI

UI jest w porządku dla jednorazowych importów poniżej tysiąca wierszy. Skrypt wygrywa, gdy tylko:

  • Importujesz więcej niż raz (np. cotygodniowy eksport z Twojego CRM)
  • Dane źródłowe wymagają oczyszczenia, zanim trafią do Brevo (deduplikacja, formatowanie numerów telefonów, dzielenie pełnych imion na imię/nazwisko)
  • Chcesz, żeby działało według harmonogramu bez klikania
  • Plik jest większy niż strefa komfortu UI

Owin powyższy skrypt w cron job lub GitHub Action i masz zautomatyzowaną synchronizację kontaktów. Następny wpis w tej serii pokazuje, jak zrobić to samo bezpośrednio z Google Sheet używając Apps Script. Bez serwera.

Dalsza lektura

Frequently Asked Questions

Jaki jest endpoint API Brevo do masowego importu kontaktów?
POST https://api.brevo.com/v3/contacts/import. Akceptuje zawartość CSV (fileBody), zdalny adres URL pliku (fileUrl) lub tablicę JSON (jsonBody). Endpoint jest asynchroniczny, zwraca processId natychmiast i kończy import w tle.
Jaki jest maksymalny rozmiar pliku?
10 MB dla inline CSV (fileBody) lub JSON (jsonBody). Dla większych plików hostuj plik w dostępnym miejscu i przekaż jego URL przez fileUrl. Ta ścieżka nie ma udokumentowanego limitu rozmiaru.
Jak zaimportować plik .xlsx?
API importu Brevo przyjmuje tylko .csv, .txt lub .json. Najpierw przekonwertuj .xlsx na .csv. Pandas, openpyxl lub LibreOffice headless zrobią to w jednej linijce. Skrypt w tym przewodniku zawiera konwersję xlsx do csv.
Czy nadpisze istniejące kontakty?
Domyślnie tak. updateExistingContacts domyślnie ma wartość true i dopasowuje po e-mailu. Ustaw na false, aby pominąć kontakty, które już istnieją. Ustaw emptyContactsAttributes na true, jeśli chcesz, by puste komórki CSV usuwały istniejące wartości (w przeciwnym razie puste komórki są ignorowane).
Zacznij za darmo z Brevo