Admin panel
Webhooks
Outbound webhooks — triggers, HMAC signatures, retries, delivery log and replay.
Webhooks push events to your systems the moment they happen. Configured in the admin panel: endpoint URL, a signing secret, and which triggers to subscribe to — optionally filtered (e.g. one community only).
Triggers
| Key | Fires when |
|---|---|
message.created |
a standard message is posted to a channel |
message.replied |
a thread reply is posted |
reaction.added |
a reaction lands on a message |
moderation.action |
warn / timeout / ban / server_ban is issued |
Direct messages never reach webhooks — DM content stays between participants.
Verifying deliveries
Anyone who learns your endpoint URL could POST fake events to it — so every delivery carries a signature only your Tyto server can produce, made with the secret you set on the webhook. Headers on each POST:
X-Tyto-Event— the trigger keyX-Tyto-Delivery— unique delivery idX-Tyto-Signature— the seal, e.g.t=1784450000,v1=a3f9c2…
To verify, redo the computation and compare:
- Split the header
tis when the delivery was signed (unix seconds);v1is the signature to check. - Rebuild the signed string
The timestamp, a literal dot, then the raw request body — before any JSON parsing:
“<t>.<body>”. - Compute HMAC-SHA256
Over that string, keyed with the webhook’s secret. The hex result must equal
v1(constant-time compare). - Check freshness
Reject deliveries whose
tis older than a few minutes — that’s what stops an attacker replaying a captured request later.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(header, rawBody, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Reliability
Failed deliveries retry with backoff; persistent failure auto-disables the webhook while new events buffer (up to the configured cap) — re-enable and the buffer replays. Every attempt is inspectable in the delivery log.
Endpoints must be external by default — targeting internal networks requires flipping the SSRF guard deliberately.