30.06.2026 10 мин чтения

Программное создание email-рассылки в 1С-Битрикс: кампании, сегменты, контакты и шаблоны

Кирилл Новожилов

Кирилл Новожилов

Автор

Программное создание 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).

Как это связано при отправке

⚠️ Важно
В форме рассылки вы выбираете не контакты напрямую, а сегменты. Сегмент — это правило отбора (коннектор): CRM-клиенты, подписчики, набор адресов и т.д.

Термины, которые путают всех

«Список адресов» в админке ≠ 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();

    
⚠️ Важно
На установках без CRM сегмента all обычно нет — getDefaultIds() вернёт пустой массив. Тогда нужен fallback: собрать все контакты в набор и создать сегмент (код ниже, раздел «Выбор сегмента»).

Рассылка (Letter)

Letter — одно письмо в цепочке: тема, статус, привязка к сегментам, ссылка на сообщение (MESSAGE_ID).

Тип канала задаётся MESSAGE_CODE:

  • mail — email
  • sms — SMS (если доступно)
  • и др.

Шаблон vs сообщение

Различайте два уровня:

Шаблон (TemplateTable) Сообщение письма (Message / MessageFieldTable)
Где в админке Маркетинг → Шаблоны Внутри редактора рассылки
Назначение Переиспользуемая заготовка HTML Конкретное содержимое этого письма
В letter TEMPLATE_TYPE, TEMPLATE_ID (опционально) MESSAGE_ID (обязательно после настройки)

При программном создании обычно:

  1. Заполняете конфигурацию сообщения (SUBJECT, EMAIL_FROM, MESSAGE)
  2. Сохраняете через $letter->getMessage()->saveConfiguration($config)
  3. Записываете MESSAGE_ID в letter

Шаблон из TemplateTable можно подключить отдельно через TEMPLATE_TYPE / TEMPLATE_ID — как в визуальном редакторе.

Почему нет «REST API рассылок»

В модуле sender нет rest.php и методов в стиле sender.letter.add. Варианты:

  1. PHP API (D7-сущности) — наш случай
  2. Свой контроллер + BX.ajax.runAction / маршрут в /local/routes/
  3. Компонентные ajax (QueryController) — внутренний API админки, не для внешних интеграций

Админка /bitrix/admin/sender_letters.php внутри делает то же самое: Entity\Letter, saveConfiguration, mergeData, save.

Пошаговый алгоритм создания рассылки

Шаг 0. Подготовка

        \Bitrix\Main\Loader::includeModule('sender');

    

Права: пользователь должен иметь доступ к email-маркетингу (как в админке).

Шаг 1. Выбор сегмента (аудитории)

Логика в трёх уровнях:

  1. Передан явный LIST_ID → сегмент для этого набора
  2. Есть системный сегмент CODE=allSegment::getDefaultIds()
  3. Иначе → набор 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().

Решение
Добавьте ящик в настройках почты / отправителей сайта или возьмите email из списка:
        $allowed = \Bitrix\Sender\Integration\Sender\AllowedSender::getList($userId);

    

Сегмент «Все» не найден

Segment::getDefaultIds() вернул пустой массив — на коробочном Битрикс без CRM нет группы с CODE = 'all'.

Решение
fallback через 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 из коробки нет.

Три правила, которые сэкономят часы отладки:

  1. Сегменты привязываются к letter, не к контактам напрямую
  2. Новое письмо — сначала LetterTable::add(), потом сегменты
  3. После add() перезагружайте Letter::createInstanceById($id)
Опубликовано 3 дня назад

Комментарии (0)

Пока нет ни одного комментария. Будьте первым!

Похожие статьи

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.