Что такое Webhook

Webhook — это механизм, который позволяет вашей системе получать автоматические уведомления о событиях через HTTP-запросы. Когда в нашей системе происходит определенное событие (например, рассылка завершена), мы отправляем POST-запрос с детальной информацией на указанный вами URL.

Настройка Webhook

Вы можете настроить вебхуки в веб-панели управления:

  1. Перейдите в раздел «Интеграции».
  2. Нажмите на карточку «Webhooks».
  3. Нажмите «Создать webhook».
  4. Заполните поля:
    • Название — для вашей личной идентификации вебхука.
    • URL — адрес вашего сервера, который будет принимать уведомления.
    • События — выберите из списка события, о которых хотите получать уведомления.
  5. Нажмите «Создать Webhook».

После создания вебхук появится в общем списке. На его карточке отображается вся ключевая информация:

  • Статус (активен / неактивен)
  • Название и URL
  • Список отслеживаемых событий
  • Время последней успешной отправки
  • Активность (общее число отправленных событий)

В меню (⋮) в углу карточки доступны следующие действия:

  • Тест
  • Редактировать
  • Активировать / Деактивировать
  • Удалить

Формат уведомлений

На ваш URL будет отправлен POST-запрос в формате JSON следующей структуры:

{
  "count": 1,
  "events": [
    {
      "eventId": "<uuid>",
      "trigger": "<entity>.<event>",
      "occurredAt": "2023-10-27T12:35:01.123Z",
      "payload": {
        "someStringField": "string",
        "numberField": 42
      }
    }
  ]
}

Поля запроса:

  • count — количество событий в текущем запросе.
  • events — массив объектов событий.

Поля каждого события в массиве events:

  • eventId — уникальный идентификатор события.
  • trigger — тип события в формате сущность.событие (см. список ниже).
  • occurredAt — время возникновения события в формате ISO 8601 (UTC).
  • payload — объект с данными, специфичными для каждого события.

Пакетная отправка событий

События отправляются пакетами (batch) для оптимизации производительности. Один HTTP-запрос может содержать несколько событий в массиве events.

Условия отправки пакета:

  • По количеству: как только накопится 500 событий, они будут отправлены одним запросом.
  • По таймауту: если за определенное время не накопилось 500 событий, пакет будет отправлен по истечении таймаута с тем количеством событий, которое успело накопиться.

Тестовое событие

При нажатии на кнопку «Тест» в меню (⋮) в углу карточки вы получите POST-запрос со следующим payload:

{
  "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "trigger": "rusender.webhook_test",
  "occurredAt": "2030-12-31T20:59:59.999Z",
  "payload": {
    "timestamp": "2030-12-31T20:59:59.999Z",
    "stringField": "someString",
    "numberField": 42,
    "booleanField": true,
    "objectField": {
      "nestedString": "value",
      "nestedNumber": 123
    },
    "arrayField": ["item1", "item2", 3, false]
  }
}

Доступные триггеры

Сущность: Почтовые рассылки (mail_distribution)

Общий Payload для событий рассылки

Для событий mail_distribution.startedmail_distribution.completedmail_distribution.moderatingmail_distribution.approved и mail_distribution.rejected используется следующая структура payload:

{
  "mailDistributionId": 12345,
  "mailDistributionName": "Название рассылки",
  "mailDistributionSubject": "Тема письма"
}
  • mail_distribution.started — рассылка запущена.
  • mail_distribution.completed — рассылка успешно завершена.
  • mail_distribution.moderating — рассылка отправлена на модерацию.
  • mail_distribution.approved — рассылка одобрена модератором.
  • mail_distribution.rejected — рассылка отклонена модератором.

mail_distribution.banned

Рассылка заблокирована. Это событие имеет расширенный payload.

Payload:

{
  "mailDistributionId": 12345,
  "mailDistributionName": "Название рассылки",
  "mailDistributionSubject": "Тема письма",
  "negative": {
    "reason": "hard_bounced",
    "explanation": "Подробное описание причины блокировки"
  }
}

В поле reason будет одна из следующих строк:

  • hard_bounced
  • soft_bounced
  • error_spam
  • complain

Сущность: Письма в почтовых рассылках (data_mail_distribution)

События, связанные с отслеживанием статуса отдельных писем в рамках почтовой рассылки.

События со стандартным Payload

Для событий data_mail_distribution.opendata_mail_distribution.complaintdata_mail_distribution.unsubscribe и data_mail_distribution.delivered используется следующая структура payload:

{
  "taskId": "UUID",
  "email": "string"
}
  • data_mail_distribution.open — письмо открыто получателем.
  • data_mail_distribution.delivered — письмо успешно доставлено.
  • data_mail_distribution.complaint — получатель пожаловался на спам.
  • data_mail_distribution.unsubscribe — получатель отписался от рассылки.

data_mail_distribution.click

Получатель кликнул по ссылке в письме.

Payload:

{
  "taskId": "UUID",
  "email": "string",
  "linkUrl": "string"
}
События с ошибками доставки

Для событий data_mail_distribution.hard_bounceddata_mail_distribution.soft_bounced и data_mail_distribution.error используется следующая структура payload:

{
  "taskId": "UUID",
  "email": "string",
  "smtpServerResponse": "string"
}
  • data_mail_distribution.hard_bounced — постоянная ошибка доставки (например, адрес не существует).
  • data_mail_distribution.soft_bounced — временная ошибка доставки (например, почтовый ящик переполнен).
  • data_mail_distribution.error — другая ошибка при отправке письма.

Сущность: API/SMTP письма (external_mail)

События, связанные с отслеживанием статуса отдельных внешних писем (транзакционные письма, отправленные через API/SMTP).

События со стандартным Payload

Для событий external_mail.openexternal_mail.deliveredexternal_mail.complaint и external_mail.unsubscribe используется следующая структура payload:

{
  "taskId": "UUID",
  "email": "string"
}
  • taskId — UUID, который вы получаете в ответе от API
  • external_mail.open — письмо открыто получателем.
  • external_mail.delivered — письмо успешно доставлено.
  • external_mail.complaint — получатель пожаловался на спам.
  • external_mail.unsubscribe — получатель отписался.

external_mail.click

Получатель кликнул по ссылке в письме.

Payload:

{
  "taskId": "UUID",
  "email": "string",
  "linkUrl": "string"
}
События с ошибками доставки

Для событий external_mail.hard_bouncedexternal_mail.soft_bounced и external_mail.error используется следующая структура payload:

{
  "taskId": "UUID",
  "email": "string",
  "smtpServerResponse": "string"
}
  • external_mail.hard_bounced — постоянная ошибка доставки (например, адрес не существует).
  • external_mail.soft_bounced — временная ошибка доставки (например, почтовый ящик переполнен).
  • external_mail.error — другая ошибка при отправке письма.

Требования к обработке

1. Ответ сервера и таймаут

Ваш сервер должен ответить на запрос в течение 30 секунд.

  • HTTP-статус 200299: означает, что событие успешно принято и обработано.
  • Любой другой статус (3xx4xx5xx) или таймаут: считается ошибкой, после чего мы будем пытаться доставить событие повторно.

2. Идемпотентность

Наша система гарантирует доставку по принципу «как минимум один раз» (at-least-once delivery). Это означает, что из-за сетевых проблем или ошибок на стороне вашего сервера одно и то же событие может быть доставлено повторно. Используйте поле eventId для защиты от дублирующей обработки.

3. Повторные попытки (Retries)

В случае ошибки доставки мы предпримем 6 повторных попыток с увеличивающимся интервалом.

ПопыткаЗадержка после предыдущей
15 минут
220 минут
380 минут
4320 минут
5506 минут
6506 минут

Всего мы можем отправить до 7 запросов на одно событие (первая отправка + 6 повторов). После последней неудачной попытки мы прекратим доставку события, и оно будет помечено как «недоставленное».

4. Требования к URL

Ваш URL-обработчик должен использовать протокол HTTPS и иметь валидный SSL-сертификат. Уведомления на HTTP-адреса доставляться не будут.

Пример обработчика

PHP

<?php
// Получаем raw-тело запроса
$input = file_get_contents('php://input');
$data = json_decode($input, true);

if (!$data || !isset($data['events']) || !is_array($data['events'])) {
    http_response_code(400); // Bad Request
    exit('Invalid payload');
}

// Обрабатываем каждое событие в пакете
foreach ($data['events'] as $event) {
    $eventId = $event['eventId'];
    $trigger = $event['trigger'];
    $payload = $event['payload'];

    // Проверяем, что событие не обработано ранее
    if (isEventProcessed($eventId)) {
        continue; // Пропускаем уже обработанное событие
    }

    // Обрабатываем событие
    switch ($trigger) {
        case 'mail_distribution.completed':
            handleMailDistributionCompleted($payload);
            break;
        case 'mail_distribution.banned':
            handleMailDistributionBanned($payload);
            break;
        case 'data_mail_distribution.open':
            handleDataMailDistributionOpen($payload);
            break;
        case 'data_mail_distribution.click':
            handleDataMailDistributionClick($payload);
            break;
        case 'external_mail.delivered':
            handleExternalMailDelivered($payload);
            break;
        // ... другие события
    }

    // Отмечаем событие как обработанное
    markEventAsProcessed($eventId);
}

http_response_code(200);
echo 'OK';
?>

Node.js (Express)

const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhook', (req, res) => {
  const { events } = req.body;

  // Обрабатываем каждое событие в пакете
  for (const event of events) {
    const { eventId, trigger, payload } = event;

    // Проверяем идемпотентность
    if (isEventProcessed(eventId)) {
      continue; // Пропускаем уже обработанное событие
    }

    // Обрабатываем событие
    switch (trigger) {
      case 'mail_distribution.completed':
        handleMailDistributionCompleted(payload);
        break;
      case 'mail_distribution.banned':
        handleMailDistributionBanned(payload);
        break;
      case 'data_mail_distribution.open':
        handleDataMailDistributionOpen(payload);
        break;
      case 'data_mail_distribution.click':
        handleDataMailDistributionClick(payload);
        break;
      case 'external_mail.delivered':
        handleExternalMailDelivered(payload);
        break;
      // ... другие события
    }

    // Отмечаем как обработанное
    markEventAsProcessed(eventId);
  }

  res.status(200).send('OK');
});

Рекомендации по разработке

1. Обрабатывайте запросы асинхронно

Ваш сервер должен ответить HTTP 200 OK как можно быстрее. Не выполняйте длительные операции (запросы к другим API, обработка файлов) прямо в момент получения вебхука. Вместо этого добавьте задачу в очередь (например, RabbitMQ, Redis, SQS) и немедленно верните ответ. Это защитит вас от таймаутов и позволит легко масштабировать обработку.

2. Будьте готовы к новым полям

В будущем в объект payload могут быть добавлены новые поля. Ваш код должен быть устойчив к этому и не падать, если в JSON появятся неизвестные ему ключи.

3. Настройте мониторинг

Настройте мониторинг вашего URL-обработчика. Оповещения помогут быстро отреагировать на проблемы и исправить их до того, как события начнут теряться после всех попыток повторной отправки.

4. Используйте тестовые окружения

Не используйте ваш основной (production) URL для отладки. Создайте отдельный вебхук для тестового или staging-окружения, чтобы безопасно вносить изменения в код.

Часто задаваемые вопросы (FAQ)

Я настроил вебхук, но не получаю уведомлений. Что делать?

Проверьте следующее:

  1. Статус в UI: Убедитесь, что вебхук «Активен».
  2. Тестовое событие: Используйте кнопку «Тест» на карточке вебхука.
  3. Публичный URL: Убедитесь, что URL доступен из интернета (не localhost).
  4. HTTPS: Убедитесь, что URL начинается с https:// и имеет валидный SSL-сертификат.
  5. Логи сервера: Проверьте логи вашего веб-сервера (Nginx, Apache) или приложения на наличие записей о входящих запросах.
Почему вы отправляете одно и то же событие (eventId) несколько раз?

Мы повторяем отправку, если не получаем от вашего сервера подтверждение об успешной доставке (ответ с HTTP-кодом 200-299) в течение 30 секунд. Это может произойти по двум основным причинам:

  1. Ваш сервер вернул ошибку (например, статус 500 Internal Server Error).
  2. Произошла сетевая проблема, из-за которой мы не получили ответ, даже если ваше приложение его отправило и успешно обработало событие.

Поэтому ваша система всегда должна быть готова к дубликатам (см. раздел «Идемпотентность»).

Гарантируется ли порядок доставки событий?

Нет, не гарантируется. Из-за сетевых особенностей или логики повторов событие, произошедшее позже, может быть доставлено раньше. Всегда ориентируйтесь на временную метку occurredAt в теле запроса, а не на порядок их получения.

Что произойдет, если мой сервер будет недоступен долгое время?

Мы будем пытаться доставить событие в течение 24 часов. Если ваш сервер останется недоступным дольше, событие будет утеряно. Планируйте технические работы с учетом этого окна.

Могу ли я использовать один и тот же URL для нескольких вебхуков?

Да. Это распространенная практика. В вашем коде используйте поле trigger для маршрутизации и вызова соответствующей логики обработки.

Дата публикации Дата публикации: 2 июля 2025 Обновлено: 11 февраля 2026