Транзакционная рассылка (для интеграторов)

Обновлено: Oct 12, 2026Раздел: Интеграции

Отправляйте по одному исходящему письму на получателя из интеграции FORM или CONTACT_FORM, используя SMTP этой интеграции (outboundMail). Вызов выполняется между серверами с X-API-Key и областью ``transactional.send``.

Это не SMTP-ретранслятор без сохранения состояния. Mailoo создаёт полный жизненный цикл отправки внутри интеграции: TransactionalSend, Message (OUTBOUND), опциональная синхронизация Subscriber, отслеживание открытий/кликов и опциональные вебхуки доставки. Подробнее об отличиях от экспорта SMTP-реквизитов см. outbound-smtp{.interpreted-text role="doc"}.

Связанные материалы: уровень SMTP --- outbound-smtp{.interpreted-text role="doc"}; токены отписки --- website-forms{.interpreted-text role="doc"}; OpenAPI --- https://api.mailoo.app/docs/v1.

Архитектура

  • Инициирование, а не ретрансляция: Ваш бэкенд вызывает Mailoo; Mailoo рендерит сообщение, сохраняет OUTBOUND Message (сначала PENDING), затем отправляет через ``integration.config.outboundMail`` (тот же SMTP, что и для ответов из панели и приветственных писем). Рендер/persist и SMTP --- отдельные шаги: сбой доставки не удаляет сохранённое письмо (статус FAILED; htmlContent остаётся для превью в панели).
  • Секреты: Используйте ``X-API-Key`` с ``transactional.send`` (или FULL) только с сервера или BFF --- никогда в браузерных сборках.
  • Идемпотентность: Каждый запрос требует ``idempotencyKey`` (макс. 256 символов, уникальный для интеграции). Первый успешный запрос возвращает 201; повторные --- 200 с idempotentReplay: true.
  • Подписчики: Mailoo создаёт или обновляет Subscriber для e-mail получателя. Если unsubscribedAt установлен, отправка пропускается (409, статус SKIPPED_UNSUBSCRIBED) и Message не создаётся.
  • События доставки: Опциональные исходящие вебхуки (delivered, opened, clicked, unsubscribed, bounced) с HMAC-подписью и повторами --- см. Вебхуки доставки ниже.

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

``POST {baseUrl}/api/v1/webhooks/transactional/{projectUid}/{integrationId}/send``

Заголовки: Content-Type: application/json, X-API-Key (область ``transactional.send`` или FULL)

Тело (JSON):

  • ``idempotencyKey`` (обязательно) --- стабильный ключ для этой логической отправки (например, daily-2026-06-20-user123)
  • ``to`` (обязательно) --- e-mail получателя
  • ``name`` (необязательно) --- отображаемое имя; используется для {{User.name}} / {{firstName}}
  • ``templateId`` (необязательно) --- id кампании (кампания панели управления на этой интеграции); используйте либо templateId, либо инлайн subject + text/html
  • ``locale`` (необязательно) --- локаль получателя для оверлеев блоков/шаблонов/слотов (как в BLOG). Если не передана или нет оверлея --- канонический EN и запись в лог. При передаче сохраняется в Subscriber.locale.
  • ``vars`` (необязательно) --- объект строковых/числовых/булевых переменных Liquid (см. ниже)
  • ``subject``, ``text``, ``html`` (необязательно) --- инлайн-контент без templateId; для инлайн-отправки обязательны subject и хотя бы одно из text или html

Ответы:

  • 201 --- письмо отправлено (status: SENT, idempotentReplay: false)
  • 200 --- идемпотентный повтор предыдущей отправки
  • 409 --- получатель отписался (status: SKIPPED_UNSUBSCRIBED)
  • 502 --- сбой SMTP-доставки (TransactionalSend status: FAILED; OUTBOUND Message сохраняется со status: FAILED и телом для превью; записывается вебхук BOUNCED)
  • 400 --- ошибка валидации
  • 403 --- у API-ключа отсутствует ``transactional.send``
  • 404 --- проект/интеграция не найдены или тип не FORM/CONTACT_FORM

Пример (инлайн):

curl -X POST "https://api.mailoo.app/api/v1/webhooks/transactional/{projectUid}/{integrationId}/send" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: mai_live_..." \
  -d '{
    "idempotencyKey": "daily-2026-06-20-user1",
    "to": "user@example.com",
    "name": "Jane Doe",
    "subject": "Hi {{firstName}}",
    "html": "<p>{{affirmationText}} <a href=\"{{writeUrl}}\">Write</a></p><p><a href=\"{{unsubLink}}\">Unsubscribe</a></p>",
    "vars": {
      "affirmationText": "You are enough.",
      "writeUrl": "https://app.example.com/write"
    }
  }'

Пример ответа (201):

{
  "success": true,
  "data": {
    "sendId": "clx...",
    "messageId": "clx...",
    "status": "SENT",
    "idempotentReplay": false
  }
}

Переменные Liquid

Шаблоны (templateId кампании или инлайн subject/text/html) рендерятся через LiquidJS перед SMTP-отправкой.

Встроенный контекст:

  • ``User.email``, ``User.name`` --- из to / name
  • ``firstName`` --- из vars.firstName или первый токен name
  • ``unsubLink`` --- URL для отписки в один клик (секрет подписи интеграции + API ``MAILOO_PUBLIC_APP_URL``)
  • ``affirmationText``, ``writeUrl``, ``streak`` --- из vars, когда указаны (часто используются в retention/nudge-потоках)

Дополнительные строковые/числовые/булевы ключи в vars включаются в контекст.

Отслеживание открытий и кликов

Если HTML-тело непустое, Mailoo внедряет:

  • Пиксель отслеживания открытий 1×1
  • Перезаписанные https:// (и //) ссылки через редиректы отслеживания кликов

Публичные маршруты (без API-ключа):

  • ``GET /api/v1/public/transactional/open/{trackingToken}`` --- возвращает прозрачный GIF; фиксирует первое открытие
  • ``GET /api/v1/public/transactional/click/{trackingToken}?u={urlencodedTarget}`` --- фиксирует клик, 302 редирект на целевой URL (неизвестный токен → 404, без редиректа)

Форма URL: один непрозрачный trackingToken на TransactionalSend (email получателя в публичном URL нет). Назначение --- percent-encoded параметр u. При обёртке HTML Mailoo декодирует &amp; (и связанные сущности) в значениях href, чтобы UTM после редиректа сохранялись.

Unique vs raw: хранится только уникальное вовлечение --- TransactionalSend.clickedAt (и CampaignRecipient.clickedAt при кампании) ставится при первом клике и не перезаписывается. Серверного raw-счётчика и per-CTA id нет. Повторный клик по-прежнему может эмитить webhook clicked с metadata.url, но счётчики в UI/API --- unique-first. Prefetch клиента или прокси (Gmail / Outlook Safe Links) может завышать unique.

Open redirect: u не подписан HMAC; известный токен можно вызвать с подменённым u. Предпочитайте ссылки в том виде, как их переписал Mailoo. Неизвестные токены не редиректят.

Установите ``MAILOO_PUBLIC_API_URL`` на хосте API, чтобы URL пикселей и кликов указывали на ваш публичный API. Установите ``MAILOO_PUBLIC_APP_URL``, когда ссылки отписки и приложения должны указывать на домен панели управления.

Вебхуки доставки

При настройке Mailoo отправляет JSON-события POST-запросом на ваш эндпоинт уведомлений.

Настройка (на уровне интеграции):

integration.config.deliveryWebhook.url и .secret --- задаются в панели управления (Вебхуки доставки на вкладке Setup интеграции) или через PATCH …/integrations/{id} с deliveryWebhook.

События: delivered (после успешного SMTP), opened, clicked, unsubscribed (после отписки; по-прежнему для legacy-потребителей), bounced (ошибка SMTP), consent.changed (булево членства в списке --- отписка и повторная подписка / Restore / смена согласия lifecycle).

Формат тела (JSON):

{
  "event": "delivered",
  "integrationId": "...",
  "messageId": "...",
  "idempotencyKey": "...",
  "recipientEmail": "user@example.com",
  "timestamp": "2026-06-20T12:00:00.000Z",
  "metadata": {}
}

consent.changed добавляет на верхнем уровне externalUserId, marketingConsentGranted и unsubscribedAt (ISO или null). См. lifecycle-events{.interpreted-text role="doc"}.

Подпись: Заголовок ``X-Mailoo-Signature: t={unixSeconds},v1={hex}``, где v1 --- HMAC-SHA256 строки "{timestamp}.{rawBody}" с использованием секрета вебхука. Заголовок ``X-Mailoo-Event`` дублирует имя перечисления (например, DELIVERED, CONSENT_CHANGED).

Повторы: Воркер очереди (интервал 10 с в процессе API) повторяет неудачные доставки до 8 раз с экспоненциальной задержкой (максимум 5 минут). Установите ``DISABLE_DELIVERY_WEBHOOK_WORKER=1`` только для тестов или при использовании отдельного процессора.

Предварительные требования

  • Интеграция FORM или CONTACT_FORM со статусом ACTIVE
  • Настроенный ``outboundMail`` (панель управления → Подключение и настройки → Исходящая почта)
  • ``MAIL_SMTP_ENCRYPTION_KEY`` на API при хранении зашифрованного SMTP-пароля или секрета отписки
  • API-ключ RESTRICTED или FULL с ``transactional.send``

Подпись отписки

Каждая интеграция FORM/CONTACT_FORM хранит зашифрованный HMAC-секрет отписки в integration.config. Mailoo автоматически генерирует {{unsubLink}} в транзакционных шаблонах (страница подтверждения под ``MAILOO_PUBLIC_APP_URL``). Владельцы могут ротировать секрет в панели управления (это инвалидирует ссылки в уже отправленных письмах). Внешние бэкенды, подписывающие собственные ссылки, должны один раз получить ротированный открытый текст или вызывать ``POST /api/v1/webhooks/unsubscribe`` с API-ключом.

RFC 8058 List-Unsubscribe: Транзакционные, кампанийные и lifecycle-отправки через sendTransactionalEmail также выставляют List-Unsubscribe и List-Unsubscribe-Post: List-Unsubscribe=One-Click на ``POST /api/v1/public/unsubscribe/one-click?email=&integrationId=&token=`` (база ``MAILOO_PUBLIC_API_URL``). Провайдеры шлют form-urlencoded List-Unsubscribe=One-Click (или пустое тело); эндпоинт идемпотентен и отвечает текстом OK. Create reply в дашборде и письма Offer Generator эти заголовки не добавляют.

📚