#Массовая рассылка контактам

📨 Сложность: medium | Скоупы: crm, im | Стек: Node.js

#Что делаем

Создаём скрипт для персонализированной рассылки уведомлений контактам из CRM. Скрипт выбирает контакты по фильтру (например, по типу или источнику), формирует персональное сообщение с подстановкой имени и отправляет уведомление через систему сообщений Bitrix24. Это удобно для информирования клиентов о акциях, изменениях или важных событиях.

#Необходимо

  • API-ключ Вайбкод с правами crm, im
  • Node.js 18+

#Полный код

javascript
// mass-messaging.js
// Персонализированная рассылка уведомлений контактам CRM

const API_KEY = process.env.VIBE_API_KEY;
const BASE_URL = process.env.VIBE_BASE_URL;

const HEADERS = {
  'X-Api-Key': API_KEY,
  'Content-Type': 'application/json',
};

// Шаблон сообщения с плейсхолдерами
const MESSAGE_TEMPLATE = `Здравствуйте, {{NAME}}!

Рады сообщить, что мы обновили условия сотрудничества.
Новые тарифы действуют с 1 числа следующего месяца.

Подробности можно узнать у вашего менеджера или на сайте.

С уважением, команда продаж`;

// Настройки рассылки
const CONFIG = {
  // Фильтр контактов
  filter: {
    typeId: 'CLIENT',      // тип: клиент
    // sourceId: 'WEB',    // источник: сайт (раскомментируйте при необходимости)
  },
  // Задержка между сообщениями (мс) — чтобы не перегружать API
  delayBetweenMessages: 1000,
  // ID отправителя (пользователь Bitrix24)
  senderId: 1,
  // Максимум контактов за один запуск
  maxContacts: 100,
};

// Загружаем контакты через Entity API
async function fetchContacts() {
  const response = await fetch(`${BASE_URL}/v1/contacts/search`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({
      filter: CONFIG.filter,
      select: ['id', 'name', 'lastName', 'assignedById'],
      sort: { id: 'asc' },
      limit: CONFIG.maxContacts,
    }),
  });

  const { data } = await response.json();
  return data || [];
}

// Подставляем данные контакта в шаблон
function personalizeMessage(template, contact) {
  const fullName = [contact.name, contact.lastName].filter(Boolean).join(' ') || 'уважаемый клиент';

  return template.replace(/\{\{NAME\}\}/g, fullName);
}

// Отправляем системное уведомление через Entity API (POST /v1/notifications)
async function sendNotification(contact, message) {
  const response = await fetch(`${BASE_URL}/v1/notifications`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({
      userId: contact.assignedById || CONFIG.senderId,
      message: `[Рассылка] Контакт: ${contact.name} ${contact.lastName}\n\n${message}`,
      type: 'system',
    }),
  });

  const result = await response.json();
  if (!result.success) {
    throw new Error(result.error?.message || 'Failed to send notification');
  }
}

// Задержка
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// Основная функция рассылки
async function runMailing() {
  console.log('📨 Запуск рассылки');
  console.log(`   Фильтр: ${JSON.stringify(CONFIG.filter)}`);
  console.log(`   Макс. контактов: ${CONFIG.maxContacts}\n`);

  // Загружаем контакты
  console.log('👥 Загружаем контакты...');
  const contacts = await fetchContacts();
  console.log(`   Найдено: ${contacts.length} контактов\n`);

  if (contacts.length === 0) {
    console.log('Контакты не найдены. Проверьте фильтр.');
    return;
  }

  // Статистика
  let sent = 0;
  let failed = 0;
  const errors = [];

  // Отправляем сообщения
  for (const contact of contacts) {
    const fullName = `${contact.name || ''} ${contact.lastName || ''}`.trim();

    try {
      const message = personalizeMessage(MESSAGE_TEMPLATE, contact);
      await sendNotification(contact, message);

      sent++;
      console.log(`   ✉️  [${sent}/${contacts.length}] ${fullName} — отправлено`);
    } catch (error) {
      failed++;
      errors.push({ contact: fullName, error: error.message });
      console.log(`   ❌ [${sent + failed}/${contacts.length}] ${fullName} — ошибка: ${error.message}`);
    }

    // Пауза между отправками
    await delay(CONFIG.delayBetweenMessages);
  }

  // Итоги
  console.log('\n' + '='.repeat(50));
  console.log('📊 Итоги рассылки:');
  console.log(`   Отправлено: ${sent}`);
  console.log(`   Ошибок: ${failed}`);
  console.log(`   Всего: ${contacts.length}`);

  if (errors.length > 0) {
    console.log('\n❌ Ошибки:');
    errors.forEach((e) => console.log(`   - ${e.contact}: ${e.error}`));
  }

  console.log('='.repeat(50));
}

runMailing().catch(console.error);

#Как это работает

  1. Скрипт загружает список контактов из CRM через Entity API (POST /v1/contacts/search) с заданным фильтром (тип контакта, источник и др.).
  2. Для каждого контакта шаблон сообщения персонализируется — подставляется имя и фамилия.
  3. Персональное сообщение отправляется как системное уведомление ответственному менеджеру контакта через POST /v1/notifications с параметром type: "system".
  4. Между отправками выдерживается пауза, чтобы не превысить лимиты API.
  5. По завершении выводится статистика: сколько сообщений отправлено, сколько с ошибками.

#Что можно улучшить

  • Добавить поддержку HTML-шаблонов с форматированием и ссылками
  • Реализовать отправку email через Entity API (POST /v1/activities) вместо системных уведомлений
  • Использовать POST /v1/contacts/search с пагинацией для обработки тысяч контактов
  • Сохранять лог рассылки в файл или базу данных для аудита