Skip to main content
Вебхуки Exode — это исходящие HTTP-уведомления: при наступлении события в платформе (регистрация, оплата, прогресс по курсу и т.д.) Exode отправляет POST-запрос на ваш URL с данными события.
Доставка идёт почти в реальном времени через очередь с повторными попытками. Приёмник должен быть идемпотентным и быстро отвечать кодом 200, 201 или 202 (таймаут ответа — 15 секунд).

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

1

Событие фиксируется

Ядро Exode генерирует доменное событие (например, PaymentCompleted) с таймстемпом и параметрами инициатора.
2

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

Exode находит активные эндпоинты, подписанные на это событие, на уровне продавца и на системном уровне. На одного продавца можно создать не более 5 эндпоинтов.
3

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

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

Отправка с подписью и повторы

Тело отправляется POST-запросом с заголовком signature (HMAC-SHA256). Если приёмник не ответил 200/201/202 — выполняется до 5 попыток с нарастающей задержкой.
Чтобы остановить доставку — переведите эндпоинт в active = false или удалите его.

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

Тело запроса — JSON-объект следующей структуры:
string
required
Имя события. Одно из значений раздела «Поддерживаемые события».
string
required
Момент возникновения события в формате ISO 8601 (UTC).
string
required
Единый ключ события. Один и тот же ключ используется для всех эндпоинтов и сохраняется между повторными попытками. Используйте его для дедупликации.
object
required
Полезная нагрузка с доменными сущностями. Состав зависит от события — см. ниже.
Структура тела
Заголовок подписи называется буквально signature (в нижнем регистре), а не X-Signature. Подпись считается от всего тела запроса, включая event, timestamp и idempotencyKey.

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

В каждом запросе передаётся заголовок signature — это HMAC-SHA256 (hex) от сырого тела запроса, подписанного секретным ключом вашего эндпоинта.
secretKey передаётся в HMAC как есть, как ASCII/UTF-8 строка из 64 символов. Несмотря на внешний вид, его не нужно декодировать из hex или base64. Результат подписи — строка из 64 hex-символов в нижнем регистре, без префикса sha256=.
Считайте HMAC именно от сырого тела запроса. Повторная сериализация JSON (с другим порядком полей, пробелами или экранированием Unicode) даст другую подпись.

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

Любой ответ, кроме 200/201/202, а также таймаут или сетевая ошибка считаются неуспехом и приводят к повторной попытке. После 5 неудачных попыток событие помечается как failed и больше не отправляется.

Управление эндпоинтами

Эндпоинты вебхуков настраиваются в админ-панели школы (раздел настроек). Для каждого эндпоинта задаётся:
  • url — HTTPS-адрес приёмника (до 255 символов);
  • events — список событий, на которые он подписан;
  • active — флаг активности (по умолчанию true);
  • note — произвольная заметка;
  • secretKey — секрет для проверки подписи, генерируется автоматически при создании эндпоинта.
Где взять секрет для проверки подписи: secretKey создаётся вместе с эндпоинтом и доступен в настройках вебхука в админ-панели школы. Скопируйте его и сохраните в переменных окружения вашего сервиса (в примерах ниже — EXODE_WEBHOOK_SECRET). Если секрет не отображается — запросите его у поддержки.
Используйте отдельные эндпоинты для независимых систем — это изолирует сбои и позволяет управлять разными наборами событий. На одного продавца — максимум 5 эндпоинтов.

Тестовая отправка

Тестовая отправка из формы сохранённого эндпоинта подписывается его же secretKey. Формат тела, алгоритм и заголовок полностью совпадают с боевыми событиями, поэтому приёмнику не нужна отдельная ветка проверки для тестовых вебхуков.
У нового, ещё не сохранённого эндпоинта постоянный secretKey пока отсутствует. Его тестовая отправка подписывается временным ключом, поэтому проверить такую подпись ключом, который появится после создания, нельзя. Чтобы проверить полный цикл верификации, сначала сохраните эндпоинт, затем отправьте тест из его формы.

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

Ниже — события, которые реально доставляются, и структура их поля data. Вложенные сущности (user, profile, course, payment и др.) описаны в справочнике объектов.
Триггер: пользователь самостоятельно завершил регистрацию.data:
  • user — объект пользователя.
  • profile — карточка профиля (null, если не заполнена).
  • states.utmSignupParams — объект UTM-меток первичного визита (опционально).
Триггер: пользователь прошёл онбординг. Структура data идентична UserSignedUp (user, profile, states.utmSignupParams).
Триггер: пользователь связал аккаунт Telegram.data:
  • user — объект пользователя (с актуальным tgId).
  • profile — карточка профиля (опционально).
  • prevTgId — предыдущий tgId для отслеживания перепривязки (number | null).
Триггер: изменился статус урока у пользователя.data:
  • userпользователь с профилем.
  • course — объект курса.
  • productпродукт курса (опционально).
  • groups — массив групп пользователя в курсе (опционально).
  • status — новый статус урока (CourseProgressLessonStatus, опционально).
  • lessonId — ID урока (опционально).
Триггер: пользователь завершил курс.data:
  • user — пользователь с профилем.
  • course — объект курса.
  • product — продукт курса (опционально).
  • groups — массив групп (опционально).
Триггер: пользователь завершил практическое задание урока.data:
  • user — пользователь с профилем.
  • course — курс (опционально).
  • lesson — урок (опционально).
  • practice — параметры практики (опционально).
  • attempt — попытка прохождения с баллами и статусом (опционально).
  • variantId — ID варианта (опционально).
Триггер: платёж успешно завершён.data.payment — объект платежа с полным деревом: invoice (счёт), invoice.user (покупатель с профилем и школой), invoice.products (позиции с продуктом/курсом, ценой и скидкой), acquiring (эквайринг и провайдер, без секретов). Денежные поля — числа.Событие отправляется только при реальном списании денег: инициализация рекуррента (привязка карты, нулевой платёж со статусом BindingCompleted) вебхук не вызывает.
Триггер: пользователю выдан бесплатный доступ к продукту.data:
Триггер: доступ к продукту выдан администратором/менеджером через LMS или API. Структура data идентична ProductEnrolledToFree (user, profile, access, product, course).
Триггер: доступ к продукту выдан после успешной оплаты. Структура data идентична ProductEnrolledToFree (user, profile, access, product, course).
Триггер: пользователю выдан сертификат за завершение курса. Событие приходит один раз на пару «пользователь + курс»: повторное завершение курса нового сертификата не создаёт.data:
  • userпользователь с профилем.
  • course — объект курса.
  • productпродукт курса (опционально).
  • certificate — объект сертификата с публичной ссылкой link (открывается без авторизации), сроком действия и слепком данных на момент выдачи.
На события UserSignedIn, UserLoggedOut, UserJoinedByReferral, UserCreatedViaLms, CourseLessonPracticeDetailedSent, CourseLessonPracticeAutoVerifySent, ProductRefundCompleted, ProductAccessSubscriptionEnding7Days и ProductAccessSubscriptionEnding1Day можно подписаться, но формальный контракт data для них ещё не зафиксирован — состав полезной нагрузки не гарантируется. Перед использованием сверьтесь с поддержкой.

Пример: PaymentCompleted

Тело ниже — реальный пример тестовой отправки (синтетические значения, точный состав полей):

Диагностика

Каждая попытка использует одну и ту же нагрузку и idempotencyKey. Задержки отсчитываются от предыдущей неудачной попытки (≈11 / 22 / 44 / 88 / 176 минут). Верните успешный код, чтобы остановить повторы.
Установите active = false, чтобы приостановить доставку без потери конфигурации и секретного ключа.
Логируйте event, timestamp, idempotencyKey и результат проверки подписи — это ускорит сверку с нашей стороной при разборе инцидентов.

Обновлено: 2026-08-03 10:28 UTC