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.
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/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "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. fileBodyijsonBodysą 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ą.
updateExistingContactsma domyślnie wartośćtrue. Jeśli ustawisz nafalse, Brevo pomija kontakty, których e-mail już istnieje. Przydatne dla zadań typu “tylko nowe”.emptyContactsAttributeskontroluje, czy puste komórki w CSV wymazują istniejące wartości. Domyślniefalse(puste komórki są ignorowane). Ustaw natrue, 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.
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 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}")Uruchom go:
python import_csv_to_brevo.py contacts.csvPierwszy 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,CITYTo 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:
- 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.
- Office Scripts + Power Automate. Jeśli skoroszyt znajduje się w OneDrive/SharePoint i chcesz nienadzorowanej zaplanowanej synchronizacji. Również omówione w przewodniku Excel.
- Konwertuj
.xlsxna 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 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")Jeśli nie chcesz pandas jako zależności, sam openpyxl też działa:
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: ta sama praca, oficjalny SDK
Brevo publikuje @getbrevo/brevo dla Node. Obsługuje uwierzytelnianie, ponawianie i typowany kształt żądania:
// 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}`);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:
# 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 . 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": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "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.