Reverse Security
Log in with Discord

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-eventevent type, e.g. incident.nuke, incident.raid, moderation.case, test
x-sentinel-event-idstable event ID (same across retries — use it to de-duplicate)
x-sentinel-timestampUnix seconds
x-sentinel-signaturesha256= + 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.