Outgoing webhooks
Signed JSON events, headers, retries and a verification sample.
Configure an HTTPS endpoint (public address, port 443) in the dashboard or with /webhook set. Private, local and link-local addresses are refused, including via DNS tricks and redirects.
Headers
x-sentinel-event | event type, e.g. incident.nuke, incident.raid, moderation.case, test |
x-sentinel-event-id | stable event ID (same across retries — use it to de-duplicate) |
x-sentinel-timestamp | Unix seconds |
x-sentinel-signature | sha256= + hex HMAC-SHA256 of timestamp + "." + raw_body |
Payload (schema version 1)
{
"schema_version": 1,
"id": "2c7d2f0e-0f3a-4b1e-9a7e-5d0b8c7a1f22",
"type": "incident.nuke",
"guild": { "id": "123456789012345678", "name": "My server", "member_count": 1250 },
"guild_id": "123456789012345678",
"culprit_id": "987654321098765432",
"adder_id": null,
"description": "5 channelDelete action(s) by <@987654321098765432> exceeded the threshold.",
"data": { "incident_id": "clx…", "module": "antinuke", "severity": 5, "attribution": "confirmed" },
"sent_at": "2026-10-03T12:00:00.000Z"
}Delivery
Respond with 2xx within 8 seconds. Failures are retried with backoff (1 min, 5 min, 30 min, 2 h, 6 h); after 6 attempts a delivery is dead-lettered. After 20 consecutive failures the endpoint is disabled until you re-enable it. Rotating the key revokes the old one immediately.
Verifying signatures
import crypto from "node:crypto";
import http from "node:http";
const SECRET = process.env.SENTINEL_WEBHOOK_SECRET; // from /webhook key
const seen = new Map(); // event id -> time, for replay protection
http.createServer((req, res) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const raw = Buffer.concat(chunks).toString("utf8"); // verify the RAW body
const ts = req.headers["x-sentinel-timestamp"];
const sig = req.headers["x-sentinel-signature"] ?? "";
const id = req.headers["x-sentinel-event-id"];
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.writeHead(400).end("stale");
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(ts + "." + raw).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(String(sig));
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.writeHead(401).end("bad signature");
if (seen.has(id)) return res.writeHead(200).end("duplicate");
seen.set(id, Date.now());
const event = JSON.parse(raw);
console.log(event.type, event.description);
res.writeHead(200).end("ok"); // answer within 8 seconds
});
}).listen(8080);A complete sample receiver is included in the repository at examples/webhook-receiver.
