Программное создание email-рассылки в 1С-Битрикс: кампании, сегменты, контакты и шаблоны
Кирилл Новожилов
Автор
Содержание
Модуль Email-маркетинг (sender) в админке выглядит просто: создал письмо, выбрал получателей, отправил. За этой формой — цепочка сущностей: кампания → сегмент → рассылка (letter) → шаблон сообщения. Если не понимать связи, API «не находится»: REST-методов нет, а первый же вызов Letter::save() падает с неочевидными ошибками.
В этом туториале — модель данных, соответствие разделам админки и полный PHP-код с нюансами, которые всплывают на практике. Исходники целиком — в первом комментарии к публикации в канале.
Что вы получите после прочтения
- Поймёте, чем отличаются список адресов, набор адресов, сегмент и кампания
- Сможете создать рассылку из PHP так же, как в
/bitrix/admin/sender_letters.php - Узнаете, почему нельзя вызывать один
save()«в лоб» и как обойти баги ядра - Получите самодостаточные фрагменты кода: выбор сегмента, создание письма, fallback без CRM
Карта админки и сущностей
| Раздел админки | URL | Сущность в коде | Таблица |
|---|---|---|---|
| Рассылки | sender_letters.php |
Bitrix\Sender\Entity\Letter |
b_sender_mailing_chain |
| Сегменты | sender_segments.php |
Bitrix\Sender\Entity\Segment |
b_sender_group |
| Кампании | sender_campaign.php |
Bitrix\Sender\Entity\Campaign |
b_sender_mailing |
| Шаблоны | sender_templates.php |
Bitrix\Sender\TemplateTable |
b_sender_template |
| Список адресов | sender_contacts.php |
Bitrix\Sender\ContactTable |
b_sender_contact |
| Наборы адресов | фильтр «Набор адресов» в контактах | Bitrix\Sender\ListTable |
b_sender_list |
Связь контакт ↔ набор: b_sender_contact_list (ContactListTable).
Как это связано при отправке

Термины, которые путают всех
«Список адресов» в админке ≠ LIST_ID в коде
Страница «Список адресов» (sender_contacts.php) — это все контакты модуля (b_sender_contact): email, телефоны, подписки, отписки.
«Набор адресов» (колонка/фильтр в гриде) — это подмножество контактов (b_sender_list). В коде это ListTable, поле коннектора — LIST_ID.
Один контакт может входить в несколько наборов. Контакт может существовать без набора — он виден на общей странице, но в сегмент с коннектором contact_list без LIST_ID не попадёт.
Кампания
Кампания (Campaign, b_sender_mailing) — контейнер для цепочки рассылок одного сайта: тематика, подписчики кампании, настройки. У каждого письма обязательно есть CAMPAIGN_ID.
Если кампании нет, ядро создаст дефолтную:
$campaignId = \Bitrix\Sender\Entity\Campaign::getDefaultId(SITE_ID);
Сегмент
Сегмент (Segment, b_sender_group) — аудитория для письма. Хранит один или несколько коннекторов в b_sender_group_connector (сериализованный ENDPOINT).
Пример коннектора «все контакты из набора #3»:
[
'MODULE_ID' => 'sender',
'CODE' => 'contact_list',
'FIELDS' => ['LIST_ID' => 3],
]
Системный сегмент «Все» (код all) — то, что админка подставляет в новую рассылку по умолчанию:
$segmentIds = \Bitrix\Sender\Entity\Segment::getDefaultIds();
all обычно нет — getDefaultIds() вернёт пустой массив. Тогда нужен fallback: собрать все контакты в набор и создать сегмент (код ниже, раздел «Выбор сегмента»).Рассылка (Letter)
Letter — одно письмо в цепочке: тема, статус, привязка к сегментам, ссылка на сообщение (MESSAGE_ID).
Тип канала задаётся MESSAGE_CODE:
mail— emailsms— SMS (если доступно)- и др.
Шаблон vs сообщение
Различайте два уровня:
Шаблон (TemplateTable) |
Сообщение письма (Message / MessageFieldTable) |
|
|---|---|---|
| Где в админке | Маркетинг → Шаблоны | Внутри редактора рассылки |
| Назначение | Переиспользуемая заготовка HTML | Конкретное содержимое этого письма |
| В letter | TEMPLATE_TYPE, TEMPLATE_ID (опционально) |
MESSAGE_ID (обязательно после настройки) |
При программном создании обычно:
- Заполняете конфигурацию сообщения (
SUBJECT,EMAIL_FROM,MESSAGE) - Сохраняете через
$letter->getMessage()->saveConfiguration($config) - Записываете
MESSAGE_IDв letter
Шаблон из TemplateTable можно подключить отдельно через TEMPLATE_TYPE / TEMPLATE_ID — как в визуальном редакторе.
Почему нет «REST API рассылок»
В модуле sender нет rest.php и методов в стиле sender.letter.add. Варианты:
- PHP API (D7-сущности) — наш случай
- Свой контроллер +
BX.ajax.runAction/ маршрут в/local/routes/ - Компонентные ajax (
QueryController) — внутренний API админки, не для внешних интеграций
Админка /bitrix/admin/sender_letters.php внутри делает то же самое: Entity\Letter, saveConfiguration, mergeData, save.
Пошаговый алгоритм создания рассылки
Шаг 0. Подготовка
\Bitrix\Main\Loader::includeModule('sender');
Права: пользователь должен иметь доступ к email-маркетингу (как в админке).
Шаг 1. Выбор сегмента (аудитории)
Логика в трёх уровнях:
- Передан явный
LIST_ID→ сегмент для этого набора - Есть системный сегмент
CODE=all→Segment::getDefaultIds() - Иначе → набор
api_all_contactsсо всеми контактами + новый сегмент
use Bitrix\Sender\ContactListTable;
use Bitrix\Sender\ContactTable;
use Bitrix\Sender\Entity\Segment;
use Bitrix\Sender\GroupConnectorTable;
use Bitrix\Sender\ListTable;
/**
* @return array{0: int[], 1: string} [ID сегментов, описание источника]
*/
function resolveSegmentIds(?int $listId): array
{
if ($listId !== null) {
return [[findOrCreateSegmentForList($listId)], "набор адресов #{$listId}"];
}
$defaultIds = Segment::getDefaultIds();
if ($defaultIds !== []) {
return [$defaultIds, 'системный сегмент «Все» (CODE=all)'];
}
$listId = findOrCreateAllContactsList();
$segmentId = findOrCreateSegmentForList($listId);
return [[$segmentId], "авто-набор #{$listId} со всеми контактами"];
}
function findOrCreateAllContactsList(): int
{
if (ContactTable::getCount() === 0) {
throw new RuntimeException(
'Нет контактов в sender. Импортируйте адреса в /bitrix/admin/sender_contacts.php'
);
}
$row = ListTable::getRow([
'filter' => ['=CODE' => 'api_all_contacts'],
'select' => ['ID'],
]);
if ($row) {
$listId = (int) $row['ID'];
} else {
$addResult = ListTable::add([
'NAME' => 'API: все контакты',
'CODE' => 'api_all_contacts',
'SORT' => 100,
]);
if (!$addResult->isSuccess()) {
throw new RuntimeException(implode('; ', $addResult->getErrorMessages()));
}
$listId = (int) $addResult->getId();
}
// Привязываем каждый контакт к набору (идемпотентно)
$contacts = ContactTable::getList(['select' => ['ID']]);
while ($contact = $contacts->fetch()) {
ContactListTable::addIfNotExist((int) $contact['ID'], $listId);
}
return $listId;
}
function findOrCreateSegmentForList(int $listId): int
{
if (!ListTable::getRowById($listId)) {
throw new InvalidArgumentException("Набор адресов #{$listId} не найден");
}
// Ищем уже существующий сегмент с этим LIST_ID
$rows = GroupConnectorTable::getList([
'select' => ['GROUP_ID', 'ENDPOINT'],
'filter' => ['=GROUP.HIDDEN' => 'N'],
]);
while ($row = $rows->fetch()) {
$endpoint = $row['ENDPOINT'];
if (
($endpoint['MODULE_ID'] ?? '') === 'sender'
&& ($endpoint['CODE'] ?? '') === 'contact_list'
&& (int) ($endpoint['FIELDS']['LIST_ID'] ?? 0) === $listId
) {
return (int) $row['GROUP_ID'];
}
}
// Создаём новый сегмент
$segment = Segment::create();
$segment->mergeData([
'NAME' => 'Набор #' . $listId,
'ENDPOINTS' => [[
'MODULE_ID' => 'sender',
'CODE' => 'contact_list',
'FIELDS' => ['LIST_ID' => $listId],
]],
])->save();
if ($segment->hasErrors()) {
throw new RuntimeException(implode('; ', $segment->getErrorMessages()));
}
return (int) $segment->getId();
}
Использование:
// Все контакты (с fallback, если нет сегмента «Все»)
$listId = null;
[$segmentIds, $segmentSource] = resolveSegmentIds($listId);
// Или конкретный набор из фильтра «Набор адресов» в sender_contacts.php
// [$segmentIds, $segmentSource] = resolveSegmentIds(3);
sender_segments.php откройте сегмент — счётчик адресов > 0.Шаг 2. Сохранить содержимое письма
$letter = \Bitrix\Sender\Entity\Letter::createInstanceById(null, [
\Bitrix\Sender\Message\iBase::CODE_MAIL,
]);
$config = $letter->getMessage()->getConfiguration();
$config->set('SUBJECT', 'Тема');
$config->set('EMAIL_FROM', 'info@example.com');
$config->set('MESSAGE', '<p>Текст</p>#UNSUBSCRIBE_LINK#');
$result = $letter->getMessage()->saveConfiguration($config);
Нюансы:
EMAIL_FROMдолжен пройтиAllowedSender::isAllowed()— список разрешённых ящиков сайта#UNSUBSCRIBE_LINK#— если у вас коробка B24 с модулемbitrix24,#UNSUBSCRIBE_LINK#в HTML обязателен при сохранении письма. На обычном интернет-магазине макрос всё равно стоит добавлять: модуль подставит ссылку отписки при отправке, это хорошая практика и для антиспама.- Плейсхолдеры персонализации:
#NAME#,#EMAIL#и др. (как в редакторе)
Шаг 3. Создать запись рассылки в БД
Здесь начинаются подводные камни. Не вызывайте сразу $letter->save() на новом объекте с сегментами.
Сначала создайте строку в b_sender_mailing_chain:
$addResult = \Bitrix\Sender\Internals\Model\LetterTable::add([
'CAMPAIGN_ID' => $campaignId,
'MESSAGE_CODE' => \Bitrix\Sender\Message\iBase::CODE_MAIL,
'MESSAGE_ID' => (string) $config->getId(),
'TITLE' => 'Рассылка из API',
'CREATED_BY' => $userId,
'UPDATED_BY' => $userId,
'IS_TRIGGER' => 'N',
'IS_ADS' => 'N',
]);
Шаг 4. Перезагрузить Letter и привязать сегменты
$letter = \Bitrix\Sender\Entity\Letter::createInstanceById($letterId, [
\Bitrix\Sender\Message\iBase::CODE_MAIL,
]);
$letter->mergeData([
'SEGMENTS_INCLUDE' => $segmentIds,
'SEGMENTS_EXCLUDE' => [],
'UPDATED_BY' => $userId,
]);
$letter->save();
b_sender_mailing_chain_group (связь letter ↔ segment).Шаг 5. Отправка (опционально)
$letter->send(); // сразу
Или настройте расписание в админке (шаг «Время») — через Dispatch\State и поля даты отправки.
После первого сохранения статус нетриггерной рассылки переходит в INIT — ядро асинхронно собирает получателей из сегментов (SegmentDataBuilder, агенты).
Типичные ошибки и симптомы
Empty primary found when trying to query Letter row
Вызван $letter->save() на новом письме с SEGMENTS_INCLUDE. Внутри saveData() вызывается saveDataSegments($id, ...), а $id ещё null — запись в b_sender_mailing_chain не создана.
LetterTable::add(), потом save() с сегментами.Не заполнено обязательное поле "MESSAGE_CODE"
Баг/особенность Letter::createInstanceById(null): в данных сущности MESSAGE_CODE = null. Второй save() пытается записать null в БД.
LetterTable::add() перезагрузить письмо: Letter::createInstanceById($letterId).Отправитель не разрешён
EMAIL_FROM не в списке AllowedSender::getList().
$allowed = \Bitrix\Sender\Integration\Sender\AllowedSender::getList($userId);
Сегмент «Все» не найден
Segment::getDefaultIds() вернул пустой массив — на коробочном Битрикс без CRM нет группы с CODE = 'all'.
findOrCreateAllContactsList() (код в шаге 1), либо укажите LIST_ID набора вручную, либо создайте сегмент в админке.В контактах 1000 адресов, в рассылку попало 50
Сегмент смотрит на набор (LIST_ID), а контакты не привязаны к набору; или сегмент «Все» на вашей редакции учитывает только CRM.
sender_contacts.php.Полный рабочий скрипт
Ниже — связка всех шагов. Функции resolveSegmentIds, findOrCreateAllContactsList, findOrCreateSegmentForList — из блока выше (шаг 1).
<?php
declare(strict_types=1);
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\Loader;
use Bitrix\Sender\Entity\Campaign;
use Bitrix\Sender\Entity\Letter;
use Bitrix\Sender\Integration\Sender\AllowedSender;
use Bitrix\Sender\Internals\Model\LetterTable;
use Bitrix\Sender\Message\iBase;
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
Loader::includeModule('sender');
$userId = (int)CurrentUser::get()->getId();
if ($userId <= 0) {
throw new RuntimeException('Нужна авторизация с правами на рассылки');
}
$campaignId = Campaign::getDefaultId(SITE_ID);
if (!$campaignId) {
throw new RuntimeException('Не удалось получить CAMPAIGN_ID');
}
$listId = null; // или int — ID набора адресов
[$segmentIds, $segmentSource] = resolveSegmentIds($listId);
$emailFrom = 'info@example.com';
if (!AllowedSender::isAllowed($emailFrom, $userId)) {
throw new InvalidArgumentException("Отправитель {$emailFrom} не разрешён");
}
// --- Шаг 2: содержимое письма ---
$letter = Letter::createInstanceById(null, [iBase::CODE_MAIL]);
$config = $letter->getMessage()->getConfiguration();
$config->set('SUBJECT', 'Тема письма');
$config->set('EMAIL_FROM', $emailFrom);
$config->set('MESSAGE', '<p>Текст рассылки</p>#UNSUBSCRIBE_LINK#');
$saveConfigResult = $letter->getMessage()->saveConfiguration($config);
if (!$saveConfigResult->isSuccess()) {
throw new RuntimeException(implode('; ', $saveConfigResult->getErrorMessages()));
}
$messageId = (int)$config->getId();
if ($messageId <= 0) {
throw new RuntimeException('Не удалось сохранить MESSAGE_ID');
}
// --- Шаг 3: запись рассылки (без сегментов!) ---
$addResult = LetterTable::add([
'CAMPAIGN_ID' => $campaignId,
'MESSAGE_CODE' => iBase::CODE_MAIL,
'MESSAGE_ID' => (string)$messageId,
'TITLE' => 'Рассылка из API',
'CREATED_BY' => $userId,
'UPDATED_BY' => $userId,
'IS_TRIGGER' => 'N',
'IS_ADS' => 'N',
]);
if (!$addResult->isSuccess()) {
throw new RuntimeException(implode('; ', $addResult->getErrorMessages()));
}
$letterId = (int)$addResult->getId();
// --- Шаг 4: перезагрузка + сегменты ---
$letter = Letter::createInstanceById($letterId, [iBase::CODE_MAIL]);
$letter->mergeData([
'SEGMENTS_INCLUDE' => $segmentIds,
'SEGMENTS_EXCLUDE' => [],
'UPDATED_BY' => $userId,
]);
if (!$letter->save() || $letter->hasErrors()) {
throw new RuntimeException(implode('; ', $letter->getErrorMessages()));
}
echo "Letter ID: {$letterId}, сегмент: #{$segmentIds[0]} ({$segmentSource})" . PHP_EOL;
// $letter->send();
Полный файл — в комментарии к посту в канале.
Расширения: что делать дальше
Обёртка в сервис модуля
Вынесите логику в /local/modules/vendor.module/lib/Application/SenderMailingService.php, зарегистрируйте в .settings.php, вызывайте из CLI-агента или контроллера.
HTTP API
// local/routes/api.php
$routes->post('/api/mailing/create/', [MailingController::class, 'create']);
В контроллере — те же шаги + ActionFilter\Authentication, Csrf, проверка прав sender.
Импорт контактов в набор
\Bitrix\Sender\ContactTable::upload($rows, false, $listId);
$rows — массив ['EMAIL' => '...', 'NAME' => '...'].
Создание сегмента с нуля
$segment = \Bitrix\Sender\Entity\Segment::create();
$segment->mergeData([
'NAME' => 'Подписчики сайта',
'ENDPOINTS' => [[
'MODULE_ID' => 'sender',
'CODE' => 'contact_list',
'FIELDS' => ['LIST_ID' => $listId],
]],
])->save();
Чек-лист перед отправкой на бою
CAMPAIGN_IDсуществует и привязан к нужномуSITE_ID- Сегмент содержит ожидаемое число адресов (счётчик в админке)
EMAIL_FROMразрешён, SPF/DKIM настроены на стороне почты- В теле письма есть
#UNSUBSCRIBE_LINK#(обязательно, если установлен модульbitrix24) - Тестовая отправка через «Проверить» в админке или на свой email
- Cron/агенты Битрикс работают (сбор получателей и очередь отправки)
Модуль sender мыслит цепочкой: кампания → letter → сообщение + сегменты → контакты. Админка и PHP API используют одни и те же классы; REST из коробки нет.
Три правила, которые сэкономят часы отладки:
- Сегменты привязываются к letter, не к контактам напрямую
- Новое письмо — сначала
LetterTable::add(), потом сегменты - После
add()перезагружайтеLetter::createInstanceById($id)
Комментарии (0)
Пока нет ни одного комментария. Будьте первым!
Похожие статьи