Как импортировать контакты из CSV или Excel в Brevo с помощью скрипта (Python, Node.js, cURL)

Массовый импорт контактов из CSV или Excel-файла в Brevo через API import-contacts. Включает готовые скрипты на Python и Node.js, однострочник на cURL, обработку ошибок и советы для файлов больше 10 MB.

Featured image for article: Как импортировать контакты из CSV или Excel в Brevo с помощью скрипта (Python, Node.js, cURL)

Если Вам когда-либо приходилось загружать несколько тысяч контактов из таблицы в Brevo, то путь через ручной интерфейс быстро становится утомительным: выбрать файл, сопоставить столбцы, выбрать список, ждать, повторить. Скрипт делает то же самое за секунды и, что важнее, его можно запускать снова по расписанию, после экспорта из базы данных или каждый раз, когда Ваш CRM выдаёт свежий CSV.

В этом руководстве рассматривается API-эндпоинт, который Brevo предоставляет именно для этой задачи, с полностью рабочими скриптами на Python, Node.js и однострочником на cURL. Всё в этом руководстве обращается к POST /v3/contacts/import.

Эндпоинт вкратце

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
}

Ответ возвращается быстро:

{ "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 при каждом запросе. Относитесь к нему как к паролю, у него полный доступ на чтение и запись в Вашем аккаунте.

Хорошая практика: положите ключ в переменную окружения, чтобы он никогда не попал в исходный код.

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

Python: прочитать CSV и отправить в Brevo

Это самый простой возможный скрипт: чтение локального CSV стандартной библиотекой и отправка тела напрямую в Brevo. Из сторонних зависимостей нужен только 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}")

Запуск:

Terminal window
python import_csv_to_brevo.py contacts.csv

Первая строка Вашего CSV - это заголовок. Имена столбцов сопоставляются с атрибутами Brevo по точному совпадению в верхнем регистре: EMAIL, FIRSTNAME, LASTNAME, плюс любые пользовательские атрибуты, которые Вы определили. EMAIL обязателен.

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

Это весь процесс. Скрипт возвращается в течение секунды; сам импорт идёт на стороне сервера и занимает от нескольких секунд до нескольких минут в зависимости от объёма.

Excel-файлы: три рабочих пути

Эндпоинт импорта Brevo не читает .xlsx напрямую, поэтому есть три реальных варианта, в зависимости от того, где должна выполняться работа:

  1. Запустить VBA-макрос внутри книги - синхронизация в один клик прямо из Excel, без внешнего скрипта. Правильный ответ, когда файл лежит на десктопе и Ваша команда хочет кнопку на листе. Полный код в руководстве по VBA-макросу для Excel и Brevo.
  2. Office Scripts + Power Automate - если книга находится в OneDrive/SharePoint и Вы хотите автономную синхронизацию по расписанию. Тоже разобрано в руководстве по Excel.
  3. Преобразовать .xlsx в CSV из скрипта - этому посвящён остаток этого раздела. Лучший вариант, если у Вас уже работает Python или Node на сервере и нужно подтягивать книгу раз в день.

Для варианта 3 pandas справляется в одну строку:

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

Если Вы не хотите тянуть pandas как зависимость, достаточно одного openpyxl:

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: та же задача, официальный SDK

Brevo публикует пакет @getbrevo/brevo для Node. Он берёт на себя авторизацию, повторные попытки и типизированную форму запроса:

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

Или, если 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: однострочник для разовых импортов

Когда Вы просто хотите протестировать эндпоинт или вручную залить небольшой файл:

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 . забирает весь файл и 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": [
{
"email": "[email protected]",
"attributes": {
"FIRSTNAME": "Jane",
"LASTNAME": "Doe",
"COMPANY": "Acme",
},
},
{
"email": "[email protected]",
"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, без сервера.

Дополнительные материалы

Frequently Asked Questions

Какой эндпоинт Brevo API отвечает за массовый импорт контактов?
POST https://api.brevo.com/v3/contacts/import. Он принимает CSV-содержимое (fileBody), URL удалённого файла (fileUrl) или JSON-массив (jsonBody). Эндпоинт асинхронный: он сразу возвращает processId и завершает импорт в фоне.
Какой максимальный размер файла?
10 MB для встроенного CSV (fileBody) или JSON (jsonBody). Для файлов большего размера разместите файл по доступному адресу и передайте его URL через fileUrl, у этого пути нет задокументированного ограничения по размеру.
Как импортировать файл .xlsx?
Импорт-API Brevo принимает только .csv, .txt или .json. Сначала преобразуйте .xlsx в .csv, это можно сделать в одну строку с помощью pandas, openpyxl или LibreOffice headless. Скрипт в этом руководстве включает конвертацию xlsx в csv.
Перезапишет ли импорт существующие контакты?
По умолчанию да, updateExistingContacts установлен в true и сопоставляет контакты по email. Установите false, чтобы пропускать уже существующие контакты. Установите emptyContactsAttributes в true, если хотите, чтобы пустые ячейки CSV стирали существующие значения (иначе пустые ячейки игнорируются).
Начните бесплатно с Brevo