Webhook 会在你团队的操作发生的那一刻,把它们发送到你自己拥有的 URL。每一个记录在你的审计日志中的操作,都可以作为带签名的 JSON POST 请求推送到你自己的服务器、你自己的机器人或内部工具:无需轮询、无需导出,也无需抓取面板。
本页的一切都由一条规则决定。Webhook 携带的是你团队的活动日志所显示的内容,仅此而已。它不是另一条有自己范围的并行数据流,也不是给机器用的更丰富的负载。如果某项内容不在那个页面上,它就不会出现在请求体中。

📍 在哪里找到它
团队设置 → Webhook,位于你团队自己的页面下。每个团队最多三个端点,每个端点都有自己的 URL、自己的类别和自己的签名密钥。
谁可以设置:团队所有者,或者角色包含将服务器从团队中移除权限的成员。这与打开活动日志本身的规则相同:Webhook 转发的是那个页面所呈现的内容,所以被允许阅读它,就等于被允许转发它。请参阅团队角色与权限。
🎯 会发送什么,以及绝不会发送什么
由你选择类别。只有你勾选的类别才会被投递:
| 类别 | 涵盖内容 |
|---|---|
| 功能 | 某项功能被启用、被禁用,或其设置被保存 |
| 服务器 | 服务器被添加到团队或从团队中移除 |
| 团队 | 团队被重命名、角色被编辑、Webhook 被更改 |
| 成员 | 成员被邀请、被添加、被移除或自行离开 |
| 管理处置 | 处罚、申诉、举报、工单、已清除的历史记录 |
| 订阅 | 团队上的订阅变更 |
| 账单 | 团队上的账单事件 |
| 平台对本团队的操作 | 平台管理员对你的某台服务器所做的操作 |
🚫 绝不会传输的三样东西
- **IP 地址。**在活动日志上,IP 只显示给它所属的那个人,不会显示给任何其他人。Webhook 没有可供核对的读者,所以这个字段不会被发送:它会从负载中被移除,而不是被置空。
- **与你无关的后台操作。**平台范围的管理操作不带团队信息,所以分发过程中的任何环节都无法触及它们。平台对本团队的操作这个类别,只包含管理员对你的服务器所做的操作。
- **凭据。**任何看起来像令牌、密钥或机密的字段,都会以
••••••••的形式送达,一对更改前/更改后的值两边都是如此。这在发出时会再次执行,独立于页面所做的遮蔽。
**不提供登录事件。**审计记录有一个 auth 类别,它被有意地排除在上面的列表之外:把登录事件无人值守且无限期地推送给第三方服务器,与把它们显示在某人必须主动打开的页面上,是两种不同的行为。以后可能会加入。
📦 投递内容
每次投递都是一个带有 JSON 请求体的 POST 请求:
{
"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 |
投递 ID,与 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 解析之前。重新序列化请求体会改变它(键的顺序、被转义的斜杠、Unicode),摘要就会对不上。
- 时间戳在被签名的字符串之内,而不只是附带在旁边。这正是让你能拒绝重放攻击的原因:拒绝时间戳超过几分钟的投递(五分钟是一个合理的窗口),攻击者就无法重新发送他们上周截获的请求。由于时间戳被签名了,他们也无法修改它。
- **以恒定时间比较。**普通的
==会在第一个不同的字节处返回,而这种耗时差异足以让人一个字节一个字节地还原出签名。请使用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都当作新请求的接收端,一定会在我们的第一次尝试在你那边成功、却在我们这边超时的那天重复计数。请保存这个 ID,并忽略你已经处理过的。 - **不保证顺序。**投递是独立排队的;被重试的投递会在较晚发生的事件之后才到达。如果顺序对你很重要,请使用
data.created_at。
⛔ 自动停用
在连续 20 次失败之后,端点会被停用,面板会在卡片上说明这一点。停止应答的端点通常是彻底停止了,而且没有人会来通知我们:无限期地重试下去,每个操作都会消耗一次请求和一行历史记录,永无止境。
要重新启动它:先修好你的接收端,然后编辑该 Webhook,并再次勾选向该地址投递事件。重新开启会清除计数器,所以修复后的端点不会再次因为一次失败就被停用。
🧪 发送测试
发送测试会立即向你的端点发出一个真实的、带签名的请求,并向你显示状态码和往返时间。它不是模拟:它走的是每次真实投递所走的同一段代码,带有相同的请求头,所以通过测试的端点,不会因为测试中从未发送过的某个请求头而拒绝生产流量。
测试请求体带有 "event": "panel.webhook.test" 和 "category": "test",所以你的接收端可以识别它。
预期会收到两次,而不是一次。点击这个按钮本身就是一个被审计的团队操作。所以你的端点会收到测试投递,并且,如果它订阅了团队类别,稍后还会收到一条针对
team.webhook.tested的普通投递。这是正确的,而不是循环。
测试永远不会计入 20 次失败的上限,也永远不会清除它。调试端点不会让它被停用。
📜 最近的尝试
最近的尝试会显示该端点最近 20 次的尝试:时间、哪个事件、第几次尝试、状态码或错误,以及耗时。失败的记录会带有你的服务器所返回内容的简短摘录,这通常是发现拒绝你的是反向代理、而不是你的代码的最快方法。
尝试记录会保留 30 天,并在每晚清理。这只是调试用的副产物,不是正式记录:审计记录本身有自己的、长得多的保留期限。
🛡️ 我们拒绝发送的地方
端点 URL 必须指向公共互联网。解析到环回地址、私有地址段、链路本地地址或云元数据地址的 URL 会被拒绝,在你保存它时会拒绝,并且在每一次发送之前都会再次检查。
第二次检查才是随着时间推移真正重要的那一次。一个在输入当天指向公共地址的域名,一个月后可能指向 127.0.0.1,而 Webhook 只要存在就会一直触发。拒绝会算作该端点的一次失败,并且不会重试:再向解析器询问三次同样的问题也不会有任何改变。
请优先使用 https 端点。签名证明的是谁发送了请求体,但它不会隐藏请求体:在普通的 http 上,你的活动内容是明文传输的。
💬 直接发送到 Discord 或 Slack 频道
把 Discord webhook URL(https://discord.com/api/webhooks/…)粘贴为端点,面板就会发送一条 Discord 消息,而不是 JSON 信封:每个条目一个嵌入消息,以描述作为标题,包含事件、类别、操作者、团队、服务器、对象,以及(如果有的话)以简短对比形式显示的更改。时间戳是该条目的时间,页脚带有投递 ID。
该嵌入消息是由上面描述的同一个信封构建的,所以它继承了那两条规则:没有 IP 地址,凭据被遮蔽。提及功能在 Discord 一侧被关闭了,所以条目中名为 @everyone 的频道或身份组只会作为文本发布,不会提及任何人。X-YAWBDB-* 请求头和签名仍然会发送,只是 Discord 会直接忽略它们。
对于它接受的消息,Discord 会返回 204 No Content,这就是最近的尝试中一行绿色记录所显示的内容。
Slack 传入 webhook URL(https://hooks.slack.com/services/…)也是一样:面板为每个条目发布一条 Slack 消息:描述加粗,然后每个事实一行,更改放在代码块中,时间戳和投递 ID 在下面。它由同一个信封构建,所以同样的两条规则依然成立,并且 Slack 为自己的命令保留的三个字符(<、>、&)会被转义,所以成员输入的名称永远不会变成 @channel 或 @here。对于它接受的消息,Slack 会返回 200 ok。Slack 的工作流和触发器 URL 不是消息:它们保留 JSON 信封,工作流可以用自己的变量把它拆开。
💡 大家用它来做什么
- 把你团队的活动发布到你自己的 Discord 或 Slack 频道中:Discord 或 Slack 的 webhook URL 可以直接使用,或者在中间放上你自己的接收端来过滤和重新格式化。
- 把记录镜像到你自己的日志存储中,并在面板的保留期之后继续保存。
- 当管理处置操作在非工作时间出现时,通知某人。
- 当某项功能设置发生变化时,触发构建、同步或备份。