Как импортировать контакты из CSV или Excel в Brevo с помощью скрипта (Python, Node.js, cURL)
Массовый импорт контактов из CSV или Excel-файла в Brevo через API import-contacts. Включает готовые скрипты на Python и Node.js, однострочник на cURL, обработку ошибок и советы для файлов больше 10 MB.
Если Вам когда-либо приходилось загружать несколько тысяч контактов из таблицы в Brevo, то путь через ручной интерфейс быстро становится утомительным: выбрать файл, сопоставить столбцы, выбрать список, ждать, повторить. Скрипт делает то же самое за секунды и, что важнее, его можно запускать снова по расписанию, после экспорта из базы данных или каждый раз, когда Ваш CRM выдаёт свежий CSV.
В этом руководстве рассматривается API-эндпоинт, который Brevo предоставляет именно для этой задачи, с полностью рабочими скриптами на Python, Node.js и однострочником на cURL. Всё в этом руководстве обращается к POST /v3/contacts/import.
Эндпоинт вкратце
POST https://api.brevo.com/v3/contacts/importContent-Type: application/jsonapi-key: YOUR_API_KEY
{ "listIds": [42], "updateExistingContacts": true, "emailBlacklist": false, "smsBlacklist": false}Ответ возвращается быстро:
{ "processId": 78 }Это 202 Accepted: Brevo принял импорт и обрабатывает его в фоне. processId будет Вашим идентификатором отслеживания, если Вы захотите опрашивать статус выполнения или настроить вебхук notifyUrl.
Несколько моментов, которые стоит учесть до написания кода:
- Тело запроса может быть в формате CSV (
fileBody), JSON (jsonBody) или URL удалённого файла (fileUrl). Выберите один вариант. CSV-форма использует точки с запятой (;) для разделения столбцов, а не запятые, это типичная ошибка. fileBodyиjsonBodyограничены 10 MB. Brevo рекомендует не превышать примерно 8 MB для надёжности. Для всего более крупного загрузите файл в S3, GCS или на любой HTTPS-хост и передайтеfileUrlвместо тела.- Пользовательские атрибуты, отсутствующие в Вашем аккаунте, тихо игнорируются. Создайте их в интерфейсе Brevo (или через Attributes API) до импорта строк, в которых они используются, иначе данные просто исчезнут.
updateExistingContactsпо умолчаниюtrue. Если установитеfalse, Brevo пропустит контакты с уже существующим email, удобно для задач “добавлять только новые”.emptyContactsAttributesуправляет тем, будут ли пустые ячейки CSV стирать существующие значения. По умолчаниюfalse(пустые ячейки игнорируются). Установитеtrue, если CSV является источником истины и пустые поля должны очищать устаревшие данные.
Получение API-ключа
Войдите в Brevo → Settings → SMTP & API → API Keys → создайте новый ключ. Он выглядит как xkeysib-.... Вы будете передавать его в HTTP-заголовке api-key при каждом запросе. Относитесь к нему как к паролю, у него полный доступ на чтение и запись в Вашем аккаунте.
Хорошая практика: положите ключ в переменную окружения, чтобы он никогда не попал в исходный код.
export BREVO_API_KEY="xkeysib-..."Python: прочитать CSV и отправить в Brevo
Это самый простой возможный скрипт: чтение локального CSV стандартной библиотекой и отправка тела напрямую в Brevo. Из сторонних зависимостей нужен только 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}")Запуск:
python import_csv_to_brevo.py contacts.csvПервая строка Вашего CSV - это заголовок. Имена столбцов сопоставляются с атрибутами Brevo по точному совпадению в верхнем регистре: EMAIL, FIRSTNAME, LASTNAME, плюс любые пользовательские атрибуты, которые Вы определили. EMAIL обязателен.
EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITYЭто весь процесс. Скрипт возвращается в течение секунды; сам импорт идёт на стороне сервера и занимает от нескольких секунд до нескольких минут в зависимости от объёма.
Excel-файлы: три рабочих пути
Эндпоинт импорта Brevo не читает .xlsx напрямую, поэтому есть три реальных варианта, в зависимости от того, где должна выполняться работа:
- Запустить VBA-макрос внутри книги - синхронизация в один клик прямо из Excel, без внешнего скрипта. Правильный ответ, когда файл лежит на десктопе и Ваша команда хочет кнопку на листе. Полный код в руководстве по VBA-макросу для Excel и Brevo.
- Office Scripts + Power Automate - если книга находится в OneDrive/SharePoint и Вы хотите автономную синхронизацию по расписанию. Тоже разобрано в руководстве по Excel.
- Преобразовать
.xlsxв CSV из скрипта - этому посвящён остаток этого раздела. Лучший вариант, если у Вас уже работает Python или Node на сервере и нужно подтягивать книгу раз в день.
Для варианта 3 pandas справляется в одну строку:
# 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")Если Вы не хотите тянуть pandas как зависимость, достаточно одного openpyxl:
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: та же задача, официальный SDK
Brevo публикует пакет @getbrevo/brevo для Node. Он берёт на себя авторизацию, повторные попытки и типизированную форму запроса:
// 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}`);Или, если SDK не нужен, обычный fetch работает так же:
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: однострочник для разовых импортов
Когда Вы просто хотите протестировать эндпоинт или вручную залить небольшой файл:
# 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 . забирает весь файл и JSON-экранирует его, избавляя Вас от ручного экранирования переводов строк и кавычек.
Файлы больше 10 MB: используйте fileUrl
fileBody ограничен 10 MB. Если Ваш дамп контактов больше, разместите файл по URL, доступному для Brevo, и передайте этот адрес:
body = { "fileUrl": "https://files.example.com/contacts/2026-04-30.csv", "listIds": [42], "updateExistingContacts": True,}Подойдёт всё, что доступно по публичному HTTPS-URL: предподписанные ссылки S3, GCS, Ваш собственный статический хост, даже raw-URL с GitHub для разовых импортов. Brevo забирает файл по Вашему URL и затем запускает импорт. Принимаемые форматы: .csv, .txt, .json.
Отправка JSON вместо CSV
Если Вы и так берёте контакты из базы данных, нет смысла гнать их через CSV, отправляйте сразу JSON:
body = { "jsonBody": [ { "attributes": { "FIRSTNAME": "Jane", "LASTNAME": "Doe", "COMPANY": "Acme", }, }, { "attributes": { "FIRSTNAME": "John", "LASTNAME": "Smith", "COMPANY": "Globex", }, }, ], "listIds": [42],}Тот же лимит 10 MB. То же асинхронное поведение.
Опрос статуса завершения
Импорт асинхронный, processId - Ваш идентификатор для его отслеживания. Есть отдельный эндпоинт для проверки статуса процесса:
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}")Для долго выполняющихся задач более чистый паттерн - notifyUrl: укажите HTTPS-эндпоинт, на который Brevo отправит POST по завершении импорта, и пропустите опрос.
body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"Распространённые ошибки и как их исправить
400 Bad Request без явной причины. Почти всегда дело в точках с запятой против запятых в CSV. Импорт Brevo ожидает ;, а не ,. Проверьте fileBody после шага конвертации.
Значения пользовательских атрибутов исчезают. Атрибут не существует в Вашем аккаунте. Создайте его в Contacts → Settings → Contact attributes до импорта или используйте Attributes API, чтобы создать его прямо из скрипта.
401 Unauthorized. Неправильное имя заголовка. Это api-key (нижний регистр, через дефис), а не Authorization или X-API-Key.
Импорт “успешен”, но контакты не появились в списке. Проверьте, что в listIds указан правильный список. Кроме того: если updateExistingContacts равен false и контакты уже существуют, Brevo молча пропускает их вместо повторного добавления в список.
Часть строк импортирована, часть нет. Brevo присылает Вам отчёт об ошибках по каждой строке на email после завершения импорта (если только Вы не задали disableNotification: true). Отчёт говорит, у каких строк были некорректные адреса, отсутствующие обязательные поля или проблемы с форматированием.
Когда использовать скрипт, а когда интерфейс
Интерфейс подходит для разовых импортов до тысячи строк. Скрипт выигрывает, как только:
- Вы импортируете больше одного раза (например, еженедельный экспорт из Вашего CRM)
- Исходные данные нужно почистить перед загрузкой в Brevo (дедупликация, форматирование номеров телефонов, разбиение полного имени на имя и фамилию)
- Вы хотите запускать процесс по расписанию без чьих-либо кликов
- Файл больше, чем интерфейс комфортно обрабатывает
Заверните вышеуказанный скрипт в cron-задачу или GitHub Action, и Вы получите автоматическую синхронизацию контактов. Следующий пост в этой серии показывает, как сделать то же самое прямо из Google-таблицы с помощью Apps Script, без сервера.