Skip to main content
Вебхуки Exode доставляют подтверждённые события в ваши внешние сервисы почти в реальном времени и используют повторные попытки с экспоненциальной задержкой. Каждая следующая попытка происходит через увеличивающуюся задержку после предыдущей неудачной попытки: 11 → 22 → 44 → 88 → 176 минут (от момента предыдущей попытки, а не от старта). Поэтому приёмщик должен быть идемпотентным и быстро отвечать кодами 200-202 (timeout на response = 15 сек.).

Как Exode доставляет вебхуки

1

Фиксация события

События UserSignedUp, PaymentCompleted и другие генерируются ядром Exode и попадают в очередь Bull с таймстэмпом и параметрами инициатора.
2

Поиск активных эндпоинтов

Затем ядро Exode ищет до пяти активных эндпоинтов в рамках школы/продавца, фильтруя их по выбранному событию.
3

Сбор полезной нагрузки

Для каждого события собираются связанные сущности (пользователь, платеж, доступ и т.д.), формируется data, общий idempotencyKey и timestamp.
4

Отправка и повторные попытки

Тело отправляется POST-запросом с заголовком signature (HMAC-SHA256) и повторяется до пяти раз, если приёмник не ответил 200-202.
Чтобы остановить доставку, установите active = false у эндпоинта или удалите его.

Ограничения и требования

  • Для одного продавца/школы можно создать не более пяти эндпоинтов, каждый со своим набором событий.
  • Url должен поддерживать HTTPS, принимать Content-Type: application/json и возвращать ответ не позднее 15 секунд.
  • Секретный ключ генерируется автоматически при создании и хранится в зашифрованном виде.
  • Всегда проверяйте idempotencyKey, чтобы отфильтровать дубликаты при повторной доставке.
  • Используйте разные эндпоинты для независимых систем, чтобы изолировать сбои и управлять разными наборами событий.

Формат webhook-сообщения

event
string
required
Имя события. Используйте значения из раздела «Поддерживаемые события».
timestamp
timestamp
required
Момент возникновения события в ISO 8601 UTC.
idempotencyKey
string
required
Общий ключ для всех попыток доставки конкретного события. Сохраняйте его, чтобы выполнять идемпотентную обработку.
data
object
required
Объект с доменными сущностями. Состав зависит от события и описан ниже.
{
  "event": "PaymentCompleted",
  "timestamp": "2025-01-18T12:44:55.812Z",
  "idempotencyKey": "7qqQL3bE0o8R8d3X",
  "data": {
    "payment": {
      "id": 98214,
      "uuid": "4ff8f2b2-2c18-4c8c-8106-620c46cb50ab",
      "status": "Completed",
      "type": "OneTime",
      "paidAt": "2025-01-18T12:43:27.101Z",
      "invoice": {
        "id": 77102,
        "user": {
          "id": 5562,
          "uuid": "3fPYNkV2Y8Lz",
          "email": "[email protected]",
          "phone": "+79991234567",
          "profile": {
            "firstName": "Ирина",
            "lastName": "Соколова",
            "role": "Student"
          },
          "school": {
            "id": 91,
            "seller": { "id": 11 }
          }
        }
      },
      "acquiring": {
        "provider": { "id": "ClickAcquiringUz" }
      },
      "paymentMethod": {
        "id": 128,
        "cardMask": "4300•••••••9201"
      }
    }
  }
}

Поддерживаемые события

Триггер: новый пользователь самостоятельно завершил регистрацию. data.user содержит полный профиль, data.profile — карточку профиля, data.states.utmSignupParams — исходные UTM-метки.
Триггер: пользователь прошёл онбординг в приложении. Полезная нагрузка идентична UserSignedUp.
Триггер: пользователь связал Telegram. data.user и data.profile содержат текущие данные, data.prevTgId помогает отследить перепривязку.
Триггер: изменился статус урока. data.user дополнен профилем, data.course включает продукт и продавца. Событие содержит идентификаторы урока и статус (CourseProgressLessonStatus).
Триггер: пользователь завершил курс. Набор данных тот же, что и для CourseProgressChanged.
Триггер: оплата успешно списана. data.payment содержит платеж, инвойс, пользователя, продукты, скидки и способ оплаты.
Триггер: пользователю выдан бесплатный доступ. data.access описывает доступ, data.product и data.course — продукт и курс, data.user и data.profile — пользователя.

Проверка подписи

Подпись передаётся в заголовке signature и формируется как HMAC-SHA256 от полного JSON тела вебхука и секретного ключа эндпоинта.
import crypto from 'crypto';

const verifySignature = (
  payload,
  secretKey,
  receivedSignature,
) => {
  const signedPayload = typeof payload === 'string'
    ? payload
    : JSON.stringify(payload);

  const signature = crypto
    .createHmac('sha256', secretKey)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(receivedSignature),
  );
};
Используйте оригинальное тело запроса для расчёта HMAC. Нельзя переупорядочивать поля или менять форматирование перед проверкой.

Диагностика и повторные попытки

Каждая попытка использует ту же нагрузку и idempotencyKey. Задержки между попытками считаются от момента предыдущей неудачной попытки: вторая попытка через 11 минут после первой, третья через 22 минуты после второй, четвёртая через 44 минуты после третьей, пятая через 88 минут после четвёртой. Если все пять попыток завершаются ошибкой, задача помечается как failed и отображается в очереди Bull (раздел Monitoring). Отправьте успешный ответ, чтобы остановить дальнейшие попытки.
Чтобы сохранить конфигурацию, установите active = false. События будут игнорироваться до повторного включения, но секретный ключ и статистика останутся.
Добавляйте логирование прихода события с event, timestamp, idempotencyKey и статусом проверки подписи. Это поможет быстро сопоставить ваши логи с внутренними логами Exode.