Brevo API: практическое руководство для разработчика
Руководство по Brevo API для разработчиков: аутентификация, базовый URL, контакты, транзакционные письма, кампании, объекты CRM, вебхуки, лимиты запросов и реальные ограничения.
Brevo предоставляет один REST API, который охватывает транзакционные сообщения, маркетинговые кампании, данные контактов и записи CRM. Чтобы получить первый ответ 201, нужно примерно две минуты. А вот на боевую интеграцию, которая не теряет данные молча, уходит заметно больше времени, потому что несколько самых важных ограничений либо не задокументированы, либо противоречат тому, что API сообщает о себе сам.
Это руководство закрывает обе половины: эндпоинты, SDK и аутентификацию, нужные в первый же день, и ограничения платформы, под которые нужно спроектировать систему до релиза.
Что покрывает Brevo API
Всё живёт на одном хосте и в одном версионном пути. Документация для разработчиков делит поверхность на четыре продуктовые области:
- Сообщения: транзакционные письма, SMS и WhatsApp, включая пакетные отправки, планирование и активность по сообщениям.
- Маркетинговая платформа: контакты, списки, сегменты и email-кампании.
- Электронная коммерция: товары, заказы и отслеживание клиентских событий.
- Диалоги: чат-виджет и программное управление перепиской.
Эти области используют один аккаунт, одну базу контактов и один ключ API. Это удобно и иногда опасно: скрипт, написанный в расчёте на тестовые данные, обращается к тем же контактам, которым уходят ваши кампании.
Транзакционные письма против маркетинговых
Эти два семейства ведут себя достаточно по-разному, чтобы их путаница стала самой частой ошибкой проектирования.
| Транзакционные | Маркетинговые | |
|---|---|---|
| Основной эндпоинт | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Адресация | Явные получатели в запросе | listIds или segmentIds |
| Триггер | Ваше приложение, в реальном времени | По расписанию или по требованию |
| Типичный профиль объёма | Непрерывный, по одному сообщению | Всплесками, одна большая рассылка |
| Позиция по лимитам | Очень высокая, 1 000 запросов в секунду на стандартных тарифах | Низкая, эндпоинты кампаний попадают под общий лимит |
Если вы всё ещё решаете, подходит ли вам Brevo в принципе, эту тему закрывает обзор платформы.
Аутентификация и управление ключами
Brevo использует обычный ключ API в нестандартном заголовке. Заголовок называется api-key, а не Authorization, и префикса Bearer нет. На этом спотыкаются почти все, кто раньше работал с другим API для рассылок.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Ключи создаются в приложении Brevo в настройках аккаунта, в разделе SMTP and API, на вкладке API keys. Давайте каждому ключу понятное имя, привязанное к системе, которая им пользуется. Значение ключа показывается ровно один раз при создании, поэтому потерянный ключ не восстанавливают, а создают заново.
Несколько практических правил:
- Выпускайте отдельный ключ на каждую среду развёртывания и на каждый сервис. Отзыв скомпрометированного ключа не должен ронять три несвязанные системы.
- Обычные ключи API действуют на весь аккаунт. Считайте любой ключ полным доступом к контактам, отправке и данным CRM.
- Brevo также поддерживает OAuth 2.0 для приложений, действующих от имени других аккаунтов Brevo; этот сценарий описан рядом с ключами в разделе authentication schemes.
- Сервер MCP, которым пользуются AI-ассистенты, работает с отдельным токеном и действительно использует заголовок bearer. Этот токен создаётся на том же экране API keys, но не взаимозаменяем с ключом REST.
Базовый URL, версии и первая запись
Базовый URL: https://api.brevo.com/v3/. Версия указывается в пути, а не в заголовке, и v3 является текущим поколением. Все пути в этом руководстве отсчитываются от этой базы.
Первая запись информативнее первого чтения, потому что она задействует те части аккаунта, которые чаще всего настроены неверно (в первую очередь подтверждённых отправителей):
curl -X POST https://api.brevo.com/v3/smtp/email \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender": { "name": "Ops", "email": "[email protected]" }, "to": [{ "email": "[email protected]", "name": "Dev" }], "subject": "Первая транзакционная отправка", "htmlContent": "<html><body><p>Работает.</p></body></html>", "tags": ["smoke-test"] }'Успешная отправка возвращает 201 и messageId. Отложенная отправка возвращает 202.
Эндпоинты, которыми вы действительно будете пользоваться
Контакты
POST /v3/contacts создаёт контакт. В теле принимаются email, карта attributes для пользовательских полей, listIds, ext_id для вашего собственного внешнего ключа и два флага, которые на практике важнее всего: updateEnabled, превращающий вызов в upsert, и getId, из-за которого ответ возвращает идентификатор контакта.
Чтение идёт через GET /v3/contacts, который постранично отдаёт данные по limit (по умолчанию 50, максимум 1000) и offset, а также поддерживает modifiedSince и createdSince в UTC. Инкрементальная синхронизация должна опираться на modifiedSince, а не обходить весь список. Учтите, что параметр filter поддерживает только оператор равенства, поэтому всё более выразительное относится к сегментам.
Для массовой загрузки POST /v3/contacts/import принимает fileUrl, fileBody или jsonBody, направляет данные в listIds и работает асинхронно, возвращая processId. Brevo документирует максимум 10 МБ на тело запроса и рекомендует держаться около 8 МБ, потому что разбор увеличивает полезную нагрузку. Указывайте notifyUrl, чтобы узнать результат, а не опрашивать статус.
Транзакционные письма
POST /v3/smtp/email является рабочей лошадкой. Помимо sender, to, subject и htmlContent, стоит знать такие поля:
templateIdвместе сparams, который заменяет встроенный контент шаблоном Brevo и подстановкой переменных. Параметры отдельной версии ограничены 100 КБ, суммарные параметры, 1000 КБ.messageVersions, который отправляет персонализированные варианты одним вызовом, до 99 получателей на версию.tags, который стоит проставлять всегда. Теги возвращаются в событиях вебхуков и являются единственным дешёвым способом связать событие доставки с породившим его участком кода.scheduledAtвместе сbatchIdдля будущих отправок, которые вы, возможно, захотите отменить группой.headersв формате Title-Case для пользовательских заголовков SMTP.
Один запрос принимает не более 2 000 получателей. О разнице между этим эндпоинтом и отправкой кампаний с точки зрения стратегии рассылок рассказывает руководство по транзакционным письмам.
Email-кампании
POST /v3/emailCampaigns требует name и sender, а также ровно один источник контента: htmlContent (минимум 10 символов, менее 1 МБ), htmlUrl или templateId. Аудитория указывается в recipients как listIds или segmentIds, а scheduledAt использует формат UTC YYYY-MM-DDTHH:mm:ss.SSSZ. Сопутствующие маршруты покрывают немедленную отправку, отправку теста, обновление статуса и получение отчёта по кампании.
Компании, сделки и объекты
В CRM от Brevo есть два пересекающихся пути записи, и правильный выбор здесь важен.
Маршруты CRM: POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id} и аналогичный набор для сделок. Они синхронные. PATCH возвращает 204 после применения изменения.
API объектов является массовым путём: POST /v3/objects/{object_type}/batch/upsert принимает до 1000 записей и 1 МБ на запрос, до 500 атрибутов на запись и до 10 связанных записей на тип объекта в каждой записи. Он возвращает 202 и processId, то есть «принято», а не «применено».
curl -X POST https://api.brevo.com/v3/objects/company/batch/upsert \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "records": [ { "identifiers": { "id": 12345 }, "attributes": { "domain": "acme.example", "industry": "retail" } } ] }'Руководство по Brevo CRM описывает модель объектов со стороны оператора.
Официальные SDK
Brevo поддерживает клиенты в организации getbrevo на GitHub:
| Язык | Репозиторий |
|---|---|
| Node.js | github.com/getbrevo/brevo-node |
| Python | github.com/getbrevo/brevo-python |
| PHP | github.com/getbrevo/brevo-php |
| Java | github.com/getbrevo/brevo-java |
| C# | github.com/getbrevo/brevo-csharp |
| Go | github.com/getbrevo/brevo-go |
| Ruby | github.com/getbrevo/brevo-ruby |
Клиент для Node устанавливается как @getbrevo/brevo:
npm install @getbrevo/brevoimport { BrevoClient } from "@getbrevo/brevo";
const brevo = new BrevoClient({ apiKey: process.env.BREVO_API_KEY });
const result = await brevo.transactionalEmails.sendTransacEmail({ subject: "Заказ подтверждён", htmlContent: "<html><body><p>Спасибо за заказ.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);Клиент для Python ставится командой pip install brevo-python. Если вы предпочитаете не тащить зависимость SDK ради двух эндпоинтов, чистая HTTP-поверхность достаточно мала, чтобы обращаться к ней напрямую, что заодно защищает от смены версий SDK:
import osimport requests
BASE = "https://api.brevo.com/v3"HEADERS = { "api-key": os.environ["BREVO_API_KEY"], "Content-Type": "application/json",}
def upsert_contact(email, attributes, list_ids): response = requests.post( f"{BASE}/contacts", headers=HEADERS, json={ "email": email, "attributes": attributes, "listIds": list_ids, "updateEnabled": True, }, timeout=30, ) response.raise_for_status() return responseЕсть также сервер MCP по адресу https://mcp.brevo.com/v1/brevo/mcp для AI-ассистентов, с аутентификацией токеном bearer, который создаётся на том же экране настроек. Он полезен для исследования и вопросов по аккаунту, но не для боевых потоков данных.
Вебхуки
Вебхуки нужны, чтобы узнавать, что произошло после отправки. POST /v3/webhooks создаёт вебхук с полями url, events, type, а также опционально channel (email или sms), batched, пользовательскими headers и объектом auth.
Есть три типа вебхуков с разными словарями событий:
- Транзакционные:
sent,request,delivered,hardBounce,softBounce,blocked,spam,invalid,deferred,click,opened,uniqueOpened,unsubscribed. - Маркетинговые:
spam,opened,click,hardBounce,softBounce,unsubscribed,listAddition,delivered,contactUpdated,contactDeleted. - Входящие:
inboundEmailProcessedиreply, которым дополнительно требуетсяdomain.
curl -X POST https://api.brevo.com/v3/webhooks \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.example/hooks/brevo", "type": "transactional", "events": ["delivered", "hardBounce", "spam", "unsubscribed"], "description": "Сигналы доставляемости" }'Три вещи, которые нужно сделать правильно. Во-первых, в аккаунте может быть не более 40 вебхуков всех типов, поэтому маршрутизируйте по событию внутри обработчика, а не регистрируйте отдельный эндпоинт на каждое событие. Во-вторых, используйте флаг batched, когда ожидаете большой объём, потому что один запрос с множеством событий обрабатывать намного дешевле, чем множество запросов. В-третьих, защитите приёмник: Brevo публикует диапазоны своих отправляющих IP-адресов, и ограничение эндпоинта этими диапазонами является документированным подходом. Добавьте собственный общий секрет через поле headers как второй слой.
Обработчики должны быть идемпотентными. Используйте связку идентификатора сообщения, типа события и метки времени в качестве ключа дедупликации.
Лимиты запросов и обработка ошибок
Лимиты Brevo заданы отдельно для каждого эндпоинта и тарифа, и разброс между эндпоинтами огромен.
| Эндпоинт | Стандартные тарифы | Professional и Enterprise |
|---|---|---|
POST /v3/smtp/email | 1 000 RPS | 2 000 RPS |
POST /v3/transactionalSMS/send | 150 RPS | 200 RPS |
/v3/contacts/... | 10 RPS, 36 000 RPH | 20 RPS, 72 000 RPH |
POST /v3/events | 10 RPS, 36 000 RPH | выше на Enterprise |
GET /v3/smtp/emails | 2 RPS, 7 200 RPH | 3 RPS, 10 800 RPH |
| Всё остальное | 100 RPH | 200 RPH |
Больнее всего именно последняя строка. Отправка практически не тарифицируется, тогда как управление кампаниями, чтение CRM и большинство административных вызовов делят бюджет в 100 запросов в час на стандартных тарифах. Наивная дозаливка, которая читает запись компании перед каждой записью, израсходует часовую квоту меньше чем за две минуты.
Каждый ответ несёт заголовки x-sib-ratelimit-limit, x-sib-ratelimit-remaining и x-sib-ratelimit-reset. Читайте их при успехе, а не только при ошибке. Превышение лимита возвращает 429, и правильная реакция: подождать интервал из заголовка reset, а затем применить экспоненциальную задержку со случайным разбросом.
async function callBrevo(path, init, attempt = 0) { const response = await fetch(`https://api.brevo.com/v3${path}`, { ...init, headers: { "api-key": process.env.BREVO_API_KEY, "Content-Type": "application/json", ...init.headers }, });
if (response.status === 429 && attempt < 5) { const reset = Number(response.headers.get("x-sib-ratelimit-reset") || 1); const backoff = Math.pow(2, attempt) * 250 + Math.random() * 250; await new Promise((r) => setTimeout(r, reset * 1000 + backoff)); return callBrevo(path, init, attempt + 1); }
return response;}Повторяйте 429 и 5xx. Никогда не повторяйте вслепую 400 или 409, потому что оба обычно означают, что запрос неверен, а не преждевременен, и 409 в особенности требует другого действия, а не повтора.
Тестирование без отправки писем
Добавьте к транзакционной отправке заголовок X-Sib-Sandbox со значением drop. Brevo проверит запрос, вернёт 201 с messageId, ничего не доставит и не запишет лог письма.
curl -X POST https://api.brevo.com/v3/smtp/email \ -H "api-key: $BREVO_API_KEY" \ -H "X-Sib-Sandbox: drop" \ -H "Content-Type: application/json" \ -d '{ "sender": { "email": "[email protected]" }, "to": [{ "email": "[email protected]" }], "subject": "Sandbox", "htmlContent": "<p>hi</p>" }'Важно понимать, что это доказывает, а что нет. Режим песочницы проверяет только формат запроса. Он ничего не говорит об аутентификации отправителя, рендеринге шаблона или доставляемости. Держите отдельный аккаунт Brevo для интеграционного тестирования всего, что затрагивает контакты или данные CRM, потому что песочница покрывает отправку, но не остальную часть API.
Ограничения, которые определяют дизайн интеграции
Это те ограничения, которые проявляются только тогда, когда интеграция работает с реальным аккаунтом под нагрузкой. Некоторые из них противоречат тому, что API сообщает о себе. Ни одно из них не обсуждается, поэтому единственный разумный ответ: спроектировать систему с их учётом.
Компаниям нужен домен, и на домен приходится только одна компания
GET /v3/crm/attributes/companies сообщает, что ни один атрибут не обязателен, а справочник по созданию компании указывает обязательным только name. На практике POST /v3/companies без непустого атрибута domain возвращает 400 с сообщением об отсутствующих обязательных атрибутах по умолчанию. Пустая строка даёт тот же результат, что и отсутствие поля.
Хуже того, уникальность домена принудительно проверяется. Вторая компания на уже занятом домене возвращает 409. Для B2B-коммерции это структурная проблема: дочерние организации с общим доменом почты покупателя не могут существовать в Brevo как отдельные компании. Более того, простой синхронизации контакта достаточно, чтобы компания появилась по домену его почты, поэтому создание может столкнуться с компанией, которую никто явно не создавал. Правильный обработчик при 409 подхватывает существующую компанию, а не падает и не повторяет запрос.
Необъявленные атрибуты отбрасываются молча
Это самое опасное поведение платформы, и Brevo описывает его прямым текстом: если атрибут присутствует в запросе, но не был заранее определён в схеме объекта, не происходит ничего. Ни ошибки, ни создания атрибута, ни предупреждения.
Поэтому ответ 2xx не является доказательством того, что данные записались. Читайте схему перед записью, отбрасывайте всё необъявленное в собственном клиенте и отказывайтесь запускать синхронизацию, атрибуты которой не существуют, вместо того чтобы месяц писать половину записи, пока кто-нибудь не заметит.
Фильтры по атрибутам принимаются и игнорируются
GET /v3/companies?filters[attributes.domain]=... возвращает 200 и игнорирует фильтр. Два совершенно разных фильтра возвращают одни и те же записи. Найти компанию по атрибуту через этот маршрут нельзя никак.
В сочетании с тем, что нефильтрованный список на больших аккаунтах отваливается по 504 при любом размере страницы, существующая компания действительно может оказаться ненаходимой документированным способом. Обходной путь: сканировать GET /v3/objects/company/records с sort=desc, что работает быстро, поддерживает постраничность и возвращает атрибуты, ограничив обход разумным числом страниц. Компания, только что вызвавшая 409, почти всегда была создана мгновением раньше, поэтому сканирование от новых к старым находит её быстро.
Один миллион записей на тип объекта и никакого массового удаления
POST /v3/objects/{type}/batch/upsert начинает возвращать 400, когда тип объекта содержит миллион записей. Блокируются не только создания, но и обновления: обращение к существующей записи по её собственному числовому идентификатору падает точно так же. Весь путь записи в объекты закрывается разом.
Вернуться под потолок медленно, потому что POST /v3/objects/{type}/batch/delete возвращает 403 для стандартных типов объектов Brevo, таких как company. Единственный маршрут: DELETE /v3/companies/{id}, по одной записи за вызов примерно за 156 мс. Очистка 124 000 записей таким способом заняла часы при 20 параллельных воркерах. Отслеживайте количество записей по расписанию, а не узнавайте о потолке из упавшей синхронизации, и направляйте высокочастотные обновления через PATCH /v3/companies/{id}, где такого лимита нет.
ext_id является идентификатором Brevo, а не вашим
В записях объектов identifiers.ext_id хранит собственный идентификатор компании в CRM Brevo, строку в стиле Mongo. Это не свободный внешний ключ. Upsert по ext_id, куда подставлен идентификатор вашей платформы, создаёт дубликаты вместо сопоставления. Ваш внешний идентификатор должен жить в отдельном объявленном атрибуте.
Upsert объектов асинхронный, записи в CRM нет
batch/upsert возвращает 202 и processId, а применяется позже. Несуществующий идентификатор падает асинхронно и всё равно возвращает вызывающей стороне 202. PATCH /v3/companies/{id} возвращает 204 и применяется синхронно. Если ваша синхронизация сообщает об успехе, это слово без дополнительного чтения заслуживает только синхронный путь.
Короткий чек-лист интеграции
- Отдельные ключи API на каждый сервис и каждую среду, с ротацией при кадровых изменениях.
- Все записи идут через один клиент, который читает заголовки лимитов и отступает при 429.
- Схема атрибутов проверяется при старте, и синхронизация отказывается запускаться, если атрибутов нет.
- 409 при создании компании означает «подхватить», а не «повторить».
- Массовые пути используют API объектов ради пропускной способности, а маршруты CRM, для всего, что должно быть подтверждено.
- Вебхуки идемпотентны, пакетированы, ограничены по IP и несут заголовок с общим секретом.
- Инкрементальная синхронизация контактов использует
modifiedSince, а не обход всего списка.
Построение и поддержка этого слоя являются настоящей инженерной работой: проверка схемы, отступы при ошибках, логика подхвата, сверка данных. Tajo существует, чтобы взять её на себя, поддерживая синхронизацию данных Shopify и коммерции с контактами, компаниями и событиями Brevo без ручного написания логики повторов и дедупликации. Если вы собираете всё сами, руководство по интеграции с Brevo разбирает решения по модели данных, которые принимаются до написания кода.
Ключевые выводы
- API является единой REST-поверхностью по адресу
https://api.brevo.com/v3/с аутентификацией через заголовокapi-key, а не через токен bearer. - Лимиты крайне неравномерны: отправка практически не тарифицируется, тогда как большинство остальных эндпоинтов делят 100 запросов в час на стандартных тарифах.
- Официальные SDK есть для семи языков, но HTTP-поверхность достаточно проста, чтобы обращаться к ней напрямую, когда нужны лишь несколько эндпоинтов.
- Режим песочницы проверяет только формат запроса, поэтому держите отдельный аккаунт для тестирования всего, что выходит за рамки отправки.
- Ответ 2xx не доказывает, что запись применилась. Необъявленные атрибуты отбрасываются молча, а upsert объектов асинхронный.
- Проектируйте систему под жёсткие ограничения: одна компания на домен, миллион записей на тип объекта, отсутствие массового удаления для стандартных объектов и фильтры по атрибутам, которые тихо ничего не делают.