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
обязательно
Имя события. Одно из значений раздела «Поддерживаемые события».
string
обязательно
Момент возникновения события в формате ISO 8601 (UTC).
string
обязательно
Единый ключ события. Один и тот же ключ используется для всех эндпоинтов и сохраняется между повторными попытками. Используйте его для дедупликации.
object
обязательно
Полезная нагрузка с доменными сущностями. Состав зависит от события — см. ниже.
Структура тела
Заголовок подписи называется буквально 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 и больше не отправляется.
Автоотключение эндпоинта. Если эндпоинт не принимает события 14 дней подряд (за это время не было ни одной успешной доставки и его не включали вручную), Exode переводит его в active = false и уведомляет владельца продавца. Новые события на такой эндпоинт не отправляются, пока его снова не включат в кабинете.

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

Эндпоинты вебхуков настраиваются в кабинете: Управление → Школа → Вебхуки (/manage/school/webhooks). Нужно право «Управление настройками школы» (SchoolManageSettings). Через SaaS API эндпоинты не управляются. Для каждого эндпоинта задаётся:
  • url — адрес приёмника (до 255 символов); используйте HTTPS;
  • events — список событий, на которые он подписан;
  • active — флаг активности (по умолчанию true);
  • note — произвольная заметка (до 255 символов);
  • secretKey — секрет для проверки подписи, генерируется автоматически при создании эндпоинта.
Где взять секрет для проверки подписи: secretKey создаётся вместе с эндпоинтом. В кабинете он копируется командой «Копировать подпись» — в меню строки эндпоинта или рядом с полем URL в его форме (несмотря на название, копируется именно секрет, а не готовая подпись). Сохраните его в переменных окружения вашего сервиса (в примерах выше — 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).
Триггер: сотрудник школы создал нового пользователя — в кабинете (по одному или массовым импортом) или через API user/create и user/upsert (только когда upsert создаёт пользователя, а не обновляет существующего). Самостоятельная регистрация пользователя приходит отдельным событием UserSignedUp.data:
Триггер: изменился статус урока у пользователя.data:
  • user — пользователь с профилем.
  • course — объект курса.
  • product — продукт курса (опционально).
  • access — доступ пользователя к продукту курса (опционально).
  • states.utmEnrollParams, states.utmSignupParams — UTM-метки записи и регистрации, см. UTM-атрибуция.
  • groups — массив групп пользователя в курсе (опционально).
  • status — новый статус урока из записи прогресса (опционально): NotStarted, OnTheory, OnPractice, OnReview, OnCorrection или Completed — расшифровка в courseProgress.
  • lessonId — ID урока (опционально).
Триггер: пользователь завершил курс.data:
  • user — пользователь с профилем.
  • course — объект курса.
  • product — продукт курса (опционально).
  • access — доступ пользователя (опционально).
  • states.utmEnrollParams, states.utmSignupParams — UTM-метки записи и регистрации, см. UTM-атрибуция.
  • 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, states).
Триггер: доступ к продукту выдан после успешной оплаты. Структура data идентична ProductEnrolledToFree (user, profile, access, product, course, states).
Триггер: пользователю выдан сертификат за завершение курса. Событие приходит один раз на пару «пользователь + курс»: повторное завершение курса нового сертификата не создаёт.data:
  • user — пользователь с профилем.
  • course — объект курса.
  • product — продукт курса (опционально).
  • certificate — объект сертификата с публичной ссылкой link (открывается без авторизации), сроком действия и слепком данных на момент выдачи.
На события UserSignedIn, UserLoggedOut, UserJoinedByReferral, CourseLessonPracticeDetailedSent, CourseLessonPracticeAutoVerifySent, ProductRefundCompleted, ProductAccessSubscriptionEnding7Days и ProductAccessSubscriptionEnding1Day можно подписаться, но формальный контракт data для них ещё не зафиксирован — состав полезной нагрузки не гарантируется. Перед использованием сверьтесь с поддержкой.

UTM-атрибуция

UTM-метки приходят в трёх местах в зависимости от события: Ключи объекта UTM: utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid, yclid, referrer, aff_id, sub_id, track_id (присутствуют только переданные). Если пользователь пришёл без меток, объект отсутствует или пуст.
Путь access.metaHistoryLogs[0].meta.utmParams, который использовали ранние интеграции, в контракте больше не отдаётся. Переключите сценарии на states.utmEnrollParams или access.meta.utmParams.

Пример: PaymentCompleted

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

Диагностика

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

Обновлено: 2026-09-28 05:04 UTC