Вебхук отправляет активность вашей команды на принадлежащий вам URL в тот же момент, когда она происходит. Каждое действие, попадающее в ваш Журнал аудита, можно отправлять как подписанный JSON-запрос POST на ваш собственный сервер, вашего собственного бота или внутренний инструмент - без опроса, без экспорта, без парсинга панели.
Одно правило определяет всё на этой странице. Вебхук несёт ровно то, что показывает Журнал активности вашей команды, и ничего больше. Не параллельную ленту с собственным охватом, не более богатую нагрузку для машин. Если этого нет на той странице, то этого нет и в теле запроса.

📍 Где его найти
Настройки команды → Вебхуки, на странице вашей команды. До трёх адресов на команду, у каждого свой URL, свои категории и свой секрет подписи.
Кто может его настроить: владелец команды или участник, чья роль включает Удалять сервер из команды. Это то же правило, которое открывает сам Журнал активности - вебхук пересылает то, что отображает эта страница, поэтому право читать её - это право пересылать. См. Роли и права команды.
🎯 Что отправляется, а что - никогда
Вы выбираете категории. Доставляются только те, которые вы отметили:
| Категория | Что охватывает |
|---|---|
| Функции | Функция включена, выключена или сохранены её настройки |
| Серверы | Сервер добавлен в команду или удалён из неё |
| Команда | Команда переименована, изменены роли, изменены вебхуки |
| Участники | Участники приглашены, добавлены, удалены или вышли |
| Модерация | Санкции, апелляции, жалобы, тикеты, очищенные истории |
| Подписка | Изменения подписки команды |
| Оплата | События оплаты команды |
| Действия платформы над этой командой | Что администратор платформы сделал с одним из ваших серверов |
🚫 Три вещи никогда не передаются
- IP-адреса. В Журнале активности IP показывается тому, кому он принадлежит, и больше никому. У вебхука нет читателя, с которым можно сверить, поэтому поле не отправляется - оно удаляется из нагрузки, а не затирается.
- Действия бэкофиса, не касающиеся вас. Администрирование всей платформы не несёт команды, поэтому ничто в рассылке не может до него дотянуться. Категория Действия платформы над этой командой - это только то, что администратор сделал с вашими серверами.
- Учётные данные. Любое поле, похожее на токен, ключ или секрет, приходит как
••••••••с обеих сторон пары до/после. Это делается ещё раз на выходе, независимо от маскировки, которую выполняет страница.
Входы в аккаунт не предлагаются. В журнале аудита есть категория auth, и она намеренно отсутствует в списке выше: отправка событий входа на сторонний сервер, без присмотра и бессрочно, - это не то же самое, что показывать их на странице, которую кому-то нужно было открыть. Возможно, она будет добавлена позже.
📦 Доставка
Каждая доставка - это один запрос POST с телом в JSON:
{
"version": 1,
"delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
"event": "server.feature.enabled",
"category": "feature",
"team": { "id": 12, "name": "Night Shift" },
"data": {
"id": 918273,
"action": "server.feature.enabled",
"category": "feature",
"description": "Enabled Starboard",
"actor_name": "Ava",
"actor_role": "moderator",
"is_admin_action": false,
"team_id": 12,
"team_name": "Night Shift",
"server_id": 4,
"server_name": "Night Shift HQ",
"subject_label": "Starboard",
"changes": { "threshold": { "before": 3, "after": 5 } },
"metadata": null,
"created_at": "2026-09-06T09:41:12+00:00"
}
}
Эти пятнадцать полей - весь data: те же пятнадцать, которые отображает Журнал активности, за вычетом IP-адреса. Любое из них может быть null: у действия без сервера "server_id": null, у действия, у которого нечего сравнивать, "changes": null. Читайте их осторожно, а не предполагайте форму для каждого события.
version- версия конверта. Она нужна, чтобы ваш получатель мог ветвиться по ней, а не гадать, когда форма расширится.event- аудируемое действие. Их больше ста тридцати, и список растёт вместе с платформой - сопоставляйте по префиксу или поcategory, а не по исчерпывающему списку названий.delivery_idостаётся одним и тем же при всех повторах одного и того же события. Именно это делает возможной дедупликацию; см. ниже.data- сама запись аудита, тот же объект, который отображает Журнал активности.
📨 Заголовки
| Заголовок | Значение |
|---|---|
User-Agent |
YAWBDB-Webhooks/1.0 |
X-YAWBDB-Event |
Название действия, то же, что event в теле |
X-YAWBDB-Delivery |
Идентификатор доставки, тот же, что delivery_id |
X-YAWBDB-Timestamp |
Unix-время в секундах, когда запрос был сформирован |
X-YAWBDB-Signature |
v1= и затем шестнадцатеричный дайджест |
🔐 Проверка подписи
Любой, кто узнает URL вашего адреса, может отправить на него POST с чем угодно. Подпись - это то, как вы отличаете наши доставки от чужих.
Дайджест - это HMAC-SHA256 от метки времени, точки и затем сырого тела запроса, с ключом - вашим секретом подписи:
signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature = "v1=" + hex( hmac_sha256(signed_string, secret) )
Три детали несут нагрузку, и пропуск любой из них оставляет дыру:
- Подписывайте сырые байты, до любого разбора JSON. Повторная сериализация тела меняет его - порядок ключей, экранированные слэши, юникод - и дайджест не совпадёт.
- Метка времени находится внутри подписываемой строки, а не просто рядом с ней. Именно это позволяет отклонять повторы: отказывайтесь от доставки, метка времени которой старше нескольких минут (разумное окно - пять минут), и злоумышленник не сможет повторно отправить запрос, перехваченный на прошлой неделе. Поскольку метка времени подписана, изменить её он тоже не может.
- Сравнивайте за постоянное время. Простое
==возвращается на первом отличающемся байте, и этой разницы во времени достаточно, чтобы восстановить подпись по одному байту. Используйтеhash_equals,crypto.timingSafeEqual,hmac.compare_digest- как это называется в вашем языке.
PHP
$raw = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';
if (abs(time() - $timestamp) > 300) {
http_response_code(400); // too old, or clock is wrong
exit;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
http_response_code(200); // accepted
Node.js (Express)
import crypto from 'node:crypto';
// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
const given = req.get('X-YAWBDB-Signature') ?? '';
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(given);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
res.sendStatus(200);
});
Префикс v1= называет схему. Если она когда-нибудь изменится, получатель, проверяющий префикс, сможет намеренно отклонить неизвестный, а не молча прочитать его неправильно.
🔑 Секрет
64 шестнадцатеричных символа, генерируются для вас, показываются ровно один раз - в панели, сразу после создания адреса или нажатия Новый секрет. Потом ничто не читает его обратно: ни страница, ни API, ни экспорт. Потеряете - создадите новый.
Смена секрета вступает в силу немедленно для всего, что отправляется с этого момента. Уже поставленные в очередь доставки продолжают уходить, подписанные старым секретом, пока очередь не опустеет, поэтому ожидайте короткого перекрытия и сначала обновите своего получателя.
🔁 Доставка, повторы и дедупликация
- Получением считается только
2xx. Всё остальное -3xx,4xx,5xx- это неудача. Отвечайте быстро, а работу делайте потом. - Перенаправления не выполняются.
302- это неудача, а не переход. Дайте нам конечный URL. - Тайм-ауты короткие: три секунды на соединение, пять секунд на ответ. Это уведомление, а не удалённый вызов процедуры - сначала подтвердите, обрабатывайте потом.
- Четыре попытки с интервалами 30 секунд, 2 минуты, затем 10 минут: примерно двенадцать минут от начала до конца. Этого хватает на деплой, но не на сбой.
- Повторы используют тот же
delivery_id. Получатель, который считает каждыйPOSTновым, будет считать дважды в тот день, когда наша первая попытка у вас удалась, но у нас завершилась тайм-аутом. Сохраняйте идентификатор и игнорируйте уже обработанный. - Порядок не гарантируется. Доставки ставятся в очередь независимо; повторно отправленная приходит после событий, случившихся позже. Если порядок для вас важен, используйте
data.created_at.
⛔ Автоматическое отключение
После 20 неудач подряд адрес отключается, и панель сообщает об этом на карточке. Адрес, переставший отвечать, обычно перестал навсегда, и никто не приходит нам об этом сказать - бесконечные повторы стоили бы запрос и строку истории на каждое действие, бессрочно.
Чтобы запустить его снова: исправьте своего получателя, затем нажмите Изменить у вебхука и снова отметьте Доставлять события на этот адрес. Именно повторное включение сбрасывает счётчик, поэтому исправленный адрес не окажется в одной неудаче от нового отключения.
🧪 Отправка теста
Отправить тест сразу отправляет на ваш адрес один настоящий подписанный запрос и показывает код статуса и время полного обращения. Это не симуляция: он проходит через тот же код, через который проходит каждая настоящая доставка, с теми же заголовками, поэтому адрес, прошедший тест, не сможет потом отклонить рабочий трафик из-за заголовка, которого тест не отправлял.
Тело теста содержит "event": "panel.webhook.test" и "category": "test", чтобы ваш получатель мог его распознать.
Ожидайте два поступления, а не одно. Нажатие кнопки само по себе является аудируемым действием команды. Поэтому ваш адрес получает тестовую доставку и - если он подписан на категорию Команда - через мгновение вторую, обычную доставку для
team.webhook.tested. Это правильно, и это не цикл.
Тест никогда не учитывается в пределе в 20 неудач и никогда его не сбрасывает. Отладка адреса не может его отключить.
📜 Последние попытки
Последние попытки показывает последние 20 попыток для этого адреса: когда, какое событие, номер попытки, код статуса или ошибку и сколько это заняло. У неудач есть короткий фрагмент того, что ответил ваш сервер, - обычно это самый быстрый способ выяснить, что отказывает обратный прокси, а не ваш код.
Попытки хранятся 30 дней и ежедневно чистятся. Это отладочный выхлоп, а не запись: у самого журнала аудита есть собственное, гораздо более долгое хранение.
🛡️ Куда мы отказываемся отправлять
URL адреса должен указывать в публичный интернет. URL, который разрешается в loopback-адрес, частный диапазон, link-local-адрес или адрес метаданных облака, отклоняется - при сохранении и ещё раз перед каждой отдельной отправкой.
Вторая проверка важна со временем. Имя, указывавшее куда-то публичное в день, когда его ввели, через месяц может указывать на 127.0.0.1, а вебхук срабатывает, пока существует. Отказ считается неудачей адреса и не повторяется: задать резолверу тот же вопрос ещё три раза ничего бы не изменило.
Предпочитайте адрес https. Подпись доказывает, кто отправил тело, но не скрывает его - по обычному http ваша активность идёт открытым текстом.
💬 Прямо в канал Discord или Slack
Вставьте URL вебхука Discord (https://discord.com/api/webhooks/…) в качестве адреса, и панель отправит сообщение Discord вместо JSON-конверта: один эмбед на запись, с описанием в заголовке, с событием, категорией, исполнителем, командой, сервером, объектом и - когда они есть - изменениями в виде короткого diff. Метка времени берётся из записи, а в подвале указан идентификатор доставки.
Эмбед строится из того же конверта, описанного выше, поэтому на него действуют те же два правила: без IP-адреса, учётные данные замаскированы. Упоминания отключены на стороне Discord, поэтому канал или роль с именем @everyone в записи публикуется как текст и никого не пингует. Заголовки X-YAWBDB-* и подпись по-прежнему отправляются; Discord их просто игнорирует.
Discord отвечает 204 No Content на принятое сообщение, и именно это показывает для него зелёная строка в Последних попытках.
То же относится к URL входящего вебхука Slack (https://hooks.slack.com/services/…): панель публикует по одному сообщению Slack на запись - описание жирным, затем по строке на каждый факт, изменения в блоке кода, а ниже метка времени и идентификатор доставки. Оно строится из того же конверта, поэтому действуют те же два правила, а три символа, которые Slack резервирует для собственных команд (<, >, &), экранируются, так что имя, введённое участником, никогда не превратится в @channel или @here. Slack отвечает 200 ok на принятое сообщение. URL workflow и trigger в Slack - это не сообщения: они сохраняют JSON-конверт, который workflow может разобрать своими переменными.
💡 Что с этим строят
- Публикуйте активность вашей команды в собственный канал Discord или Slack - URL вебхука Discord или Slack работает как есть, а можно поставить между ними собственный получатель, чтобы фильтровать и переформатировать.
- Зеркалируйте журнал в собственное хранилище логов и храните его дольше окна хранения панели.
- Вызывайте кого-нибудь, когда действие модерации происходит в нерабочее время.
- Запускайте сборку, синхронизацию или резервное копирование, когда меняется настройка функции.
🔗 См. также
- Журнал аудита - что записывает журнал и что он намеренно не записывает
- Роли и права команды - кто может открыть страницу
- Логи сервера - журнал на стороне Discord, другая функция, отвечающая на другой вопрос