A Webhook alert channel POSTs JSON to your HTTPS URL when an alert fires or resolves and for the weekly report. Create one under Alerts, Channels (Operator: one, Network: unlimited). The signing secret is shown once when you create the channel; you can rotate it later.
Request
POST with Content-Type: application/json and User-Agent: ServerStats-Webhooks/1.0, plus:
| Header | Value |
|---|---|
X-ServerStats-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-ServerStats-Event | alert.fired, alert.resolved, alert.test or report.weekly |
X-ServerStats-Delivery | A delivery id, the same on every retry, so you can deduplicate |
The signature is HMAC-SHA256(secret, t + "." + raw_body) as lowercase hex, where t is the number in the header and raw_body is the exact bytes received.
Body
{"id":"1842","type":"alert.fired","created_at":"2026-10-06T09:41:00.000Z","url":"https://serverstats.cc/app/#/all/alerts",
"data":{"v":1,"event":"alert.fired","kind":"server_down","title":"survival is offline","text":"No data for 5 minutes.",
"severity":"critical","fields":[["Server","survival"]],"ts":"2026-10-06T09:41:00.000Z",
"org":{"id":"...","name":"Example Network"},"rule":{"id":"...","name":"Server offline"},
"server":{"id":"...","name":"survival"},"subject":"..."}}alert.test is sent by Send test. report.weekly carries the report's findings and week. Branch on type, and ignore fields you do not know.
Verify
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // reject replays
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 || "");
return a.length === b.length && timingSafeEqual(a, b);
}Python:
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Verify against the raw body before parsing JSON.
URL rules and delivery
- HTTPS only, no username or password in the URL, port 443 or 8443, and the host must resolve to a public address (private, loopback and link-local ranges are refused). Redirects are not followed.
- The request times out after 10 seconds, and ServerStats keeps at most 64 KB of your response.
- Any
2xxis success. A408,429or5xx, a timeout or a network error is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours (a numericRetry-Afteris honoured). Anything else, including redirects and other 4xx statuses, fails permanently and is shown as the channel's last error.
Other webhooks you can receive
Store providers (Tebex, PayNow, CraftingStore) send webhooks to ServerStats; see store integrations. Slack, Telegram and Discord channels use those services' own formats, not this one.