Webhookは、チームのアクティビティを、発生した瞬間に、あなたが所有するURLへ送ります。監査ログに記録されるすべての操作は、署名付きのJSONの POST として、あなた自身のサーバー、ボット、社内ツールにプッシュできます。ポーリングも、エクスポートも、パネルのスクレイピングも不要です。
**このページのすべてを決める1つのルール。**Webhookが運ぶのは、チームの操作履歴に表示される内容そのままで、それ以上のものはありません。独自の範囲を持つ別のフィードでも、機械向けのより豊富なペイロードでもありません。そのページに載っていなければ、本文にも含まれません。

📍 設定できる場所
チーム自身のページにある、チーム設定 → Webhookです。チームごとに最大3つのエンドポイントを設定でき、それぞれに独自のURL、独自のカテゴリ、独自の署名シークレットがあります。
設定できる人:チームのオーナー、またはロールにサーバーをチームから外す権限が含まれているメンバーです。これは、操作履歴そのものを開けるのと同じルールです。Webhookは、そのページが表示する内容を転送するので、読む権限があることは、転送する権限があることを意味します。チームのロールと権限をご覧ください。
🎯 送信されるもの、決して送信されないもの
カテゴリは、あなたが選びます。チェックを入れたものだけが配信されます。
| カテゴリ | 対象となる内容 |
|---|---|
| 機能 | 機能の有効化、無効化、または設定の保存 |
| サーバー | チームへのサーバーの追加または削除 |
| チーム | チーム名の変更、ロールの編集、Webhookの変更 |
| メンバー | メンバーの招待、追加、削除、退出 |
| モデレーション | 処分、異議申し立て、レポート、チケット、履歴の消去 |
| サブスクリプション | チームのサブスクリプションの変更 |
| 請求 | チームの請求に関するイベント |
| このチームに対するプラットフォームの操作 | プラットフォームの管理者が、あなたのサーバーの1つに対して行った操作 |
🚫 決して送られない3つのもの
- **IPアドレス。**操作履歴では、IPはその持ち主にだけ表示され、ほかの誰にも表示されません。Webhookには、照らし合わせる読み手がいないので、このフィールドは送られません。空欄にするのではなく、ペイロードから取り除かれます。
- **あなたに関係のないバックオフィスの操作。**プラットフォーム全体の管理にはチームが紐づいていないので、ファンアウトの中にそこへ到達するものはありません。このチームに対するプラットフォームの操作のカテゴリは、管理者があなたのサーバーに対して行ったことだけです。
- **認証情報。**トークン、キー、シークレットのように見えるフィールドは、変更前と変更後のペアの両側で
••••••••として届きます。これは、ページが行うマスキングとは別に、送信時にもう一度行われます。
**サインインは対象外です。**監査証跡には auth カテゴリがありますが、上の一覧には意図的に含まれていません。サードパーティのサーバーに、人の手を介さずに無期限でサインインのイベントをプッシュすることは、誰かが開かなければならないページに表示することとは別の行為だからです。のちに追加される可能性があります。
📦 配信
各配信は、JSONの本文を持つ1回の 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"
}
}
これらの15個のフィールドが data のすべてです。操作履歴が表示するのと同じ15個から、IPアドレスを除いたものです。どれも null になることがあります。サーバーのない操作には "server_id": null が、差分のない操作には "changes": null が入ります。イベントごとに形が決まっていると思い込まず、防御的に読み取ってください。
- **
version**はエンベロープのバージョンです。形が増えたときに、推測するのではなく、受信側がそれで分岐できるようにするためにあります。 - **
event**は、監査された操作です。130種類を超え、プラットフォームとともに増え続けます。名前の網羅的なリストではなく、プレフィックスまたは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= に続く、16進数のダイジェスト |
🔐 署名の検証
エンドポイントのURLを知った人なら誰でも、そこに何でも POST できます。署名は、私たちからの配信とそれ以外を見分けるための仕組みです。
ダイジェストは、タイムスタンプ、ドット、生のリクエスト本文を連結したものに対する HMAC-SHA256 で、署名シークレットをキーにします。
signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature = "v1=" + hex( hmac_sha256(signed_string, secret) )
3つの細かい点が重要で、どれか1つでも省くと穴ができます。
- **生のバイト列に署名してください。**JSONをパースする前のものです。本文を再シリアライズすると、キーの順序、エスケープされたスラッシュ、Unicodeなどが変わり、ダイジェストが一致しなくなります。
- タイムスタンプは、署名される文字列の内側にあり、単に横に添えられているだけではありません。これによってリプレイを拒否できます。タイムスタンプが数分より古い配信を拒否すれば(5分が妥当な幅です)、攻撃者が先週キャプチャしたリクエストを再送することはできません。タイムスタンプは署名されているので、書き換えることもできません。
- **一定時間で比較してください。**単純な
==は、最初に異なるバイトで戻るため、その時間差だけで署名を1バイトずつ復元できてしまいます。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= というプレフィックスは、方式の名前です。将来これが変わった場合、プレフィックスを確認している受信側は、未知の方式を黙って誤読するのではなく、意図的に拒否できます。
🔑 シークレット
16進数の64文字で、自動で生成され、一度だけ表示されます。エンドポイントを作成した直後、または新しいシークレットを押した直後に、パネルに表示されます。その後は、ページからも、APIからも、エクスポートからも、読み戻すことはできません。なくした場合は、新しく生成します。
ローテーションは、その瞬間から送信されるすべてのものに、すぐに反映されます。すでにキューに入っている配信は、キューが空になるまで、古いシークレットで署名されたまま送られ続けます。そのため、短い重なりが生じるので、先に受信側を更新してください。
🔁 配信、再試行、重複排除
- **
2xxだけが、受信したと見なされます。**それ以外は、3xx、4xx、5xxを含め、すべて失敗です。すばやく応答し、処理はそのあとで行ってください。 - リダイレクトはたどりません。
302は、中継ではなく失敗です。最終的なURLを指定してください。 - タイムアウトは短く、接続に3秒、応答に5秒です。これは通知であり、リモートプロシージャコールではありません。先に受領を返し、処理はあとにしてください。
- 試行は4回で、30秒、2分、10分の間隔を空けます。始めから終わりまでで、約12分です。デプロイには対応できますが、障害には対応できません。
- **再試行では、同じ
delivery_idが再利用されます。**すべてのPOSTを新しいものとして扱う受信側は、こちらの最初の試行があなたの側では成功し、こちらの側ではタイムアウトした日に、二重にカウントします。IDを保存して、すでに処理したものは無視してください。 - **順序は保証されません。**配信は個別にキューに入れられます。再試行されたものは、あとから起きたイベントの後に届きます。順序が重要な場合は、
data.created_atを使ってください。
⛔ 自動停止
20回連続で失敗すると、エンドポイントは停止され、パネルのカードにその旨が表示されます。応答しなくなったエンドポイントは、たいてい永久に止まっていて、誰もそれを知らせに来ません。永遠に再試行を続けると、操作のたびに、リクエストと履歴の行が無期限に消費されてしまいます。
再開するには、受信側を直してから、Webhookの編集を開き、この宛先へイベントを配信するにもう一度チェックを入れます。再びオンにすることで、カウンターがクリアされるので、修復したエンドポイントが、もう1回失敗しただけで再び停止してしまうことはありません。
🧪 テストを送る
テスト送信は、署名付きの本物のリクエストを、すぐにエンドポイントへ1件送り、ステータスコードと往復の時間を表示します。これはシミュレーションではありません。実際の配信すべてが通るのと同じコードを通り、同じヘッダーが付くので、テストに通ったエンドポイントが、テストでは送られなかったヘッダーが原因で、本番のトラフィックを拒否することはありません。
テストの本文には "event": "panel.webhook.test" と "category": "test" が入っているので、受信側で見分けられます。
**届くのは1回ではなく、2回です。**ボタンを押すこと自体が、監査されるチームの操作だからです。そのため、エンドポイントにはテストの配信が届き、さらに、チームカテゴリを購読している場合は、少しあとに
team.webhook.testedの通常の配信がもう1件届きます。これは正しい動作であり、ループではありません。
テストは、20回の失敗の上限にはカウントされず、クリアもされません。エンドポイントをデバッグしていて、停止してしまうことはありません。
📜 最近の試行
最近の試行には、そのエンドポイントの直近20回の試行が表示されます。日時、どのイベントか、何回目の試行か、ステータスコードまたはエラー、かかった時間です。失敗した試行には、あなたのサーバーが返した内容の短い抜粋が付きます。多くの場合、拒否しているのがあなたのコードではなくリバースプロキシだと判明するのに、これが一番早い方法です。
試行の記録は30日間保持され、毎晩削除されます。これはデバッグ用の副産物であって、記録ではありません。監査証跡自体は、はるかに長い独自の保持期間を持っています。
🛡️ 送信を拒否する宛先
エンドポイントのURLは、公開されたインターネットを指している必要があります。ループバックアドレス、プライベート範囲、リンクローカルアドレス、クラウドのメタデータアドレスに解決されるURLは、保存するときに拒否され、さらに、送信のたびにもう一度拒否されます。
時間が経つにつれて重要になるのは、2回目の確認です。入力した日には公開された場所を指していた名前が、1か月後には 127.0.0.1 を指すこともあり、Webhookは存在する限り発火し続けます。拒否はエンドポイントにとって失敗として数えられ、再試行されません。リゾルバーに同じ質問をあと3回しても、何も変わらないからです。
https のエンドポイントをおすすめします。署名は、本文を誰が送ったかを証明しますが、本文を隠すものではありません。プレーンな http では、アクティビティがそのまま平文で流れます。
💬 DiscordやSlackのチャンネルに直接送る
エンドポイントとしてDiscordのWebhook URL(https://discord.com/api/webhooks/…)を貼り付けると、パネルはJSONのエンベロープの代わりにDiscordのメッセージを送ります。1件のエントリーにつき1つの埋め込みで、タイトルは説明文、そこにイベント、カテゴリ、実行者、チーム、サーバー、対象、そして(ある場合は)変更点が短い差分として表示されます。タイムスタンプはエントリーのもので、フッターには配信IDが入ります。
埋め込みは、上で説明したのと同じエンベロープから作られるので、2つのルールがそのまま適用されます。IPアドレスは含まれず、認証情報はマスクされます。メンションはDiscord側で無効にされているため、エントリーに含まれる @everyone という名前のチャンネルやロールは、テキストとして投稿され、誰にも通知されません。X-YAWBDB-* ヘッダーと署名もそのまま送られますが、Discordは単に無視します。
Discordは、受け付けたメッセージに204 No Contentで応答します。最近の試行で緑色の行になるのは、これです。
SlackのIncoming Webhook URL(https://hooks.slack.com/services/…)でも同じです。パネルは、エントリーごとに1件のSlackメッセージを投稿します。説明を太字で、続けて事実ごとに1行、変更点をコードブロックで、その下にタイムスタンプと配信IDを表示します。同じエンベロープから作られるので、同じ2つのルールが適用されます。また、Slackが自分のコマンド用に予約している3つの文字(<、>、&)はエスケープされるので、メンバーが入力した名前が @channel や @here に変わることはありません。Slackは、受け付けたメッセージに200 okで応答します。SlackのワークフローやトリガーのURLはメッセージではありません。これらはJSONのエンベロープのままで、ワークフローが独自の変数で分解できます。
💡 活用例
- チームのアクティビティを、自分のDiscordやSlackのチャンネルに投稿します。DiscordやSlackのWebhook URLはそのまま使えます。間に自前の受信側を置いて、絞り込みや整形をすることもできます。
- 証跡を自分のログストアにミラーして、パネルの保持期間を超えて保管します。
- 営業時間外にモデレーションの操作があったときに、誰かに通知します。
- 機能の設定が変わったときに、ビルド、同期、バックアップを実行します。
🔗 関連ページ
- 監査ログ: 証跡が記録するもの、そして意図的に記録しないもの
- チームのロールと権限: ページを開ける人
- サーバーログ: Discord側のログ。別の疑問に答える、別の機能です