Webhooks and signature verification

Signed JSON POSTs for alerts and the weekly report, the headers, the HMAC-SHA256 signature and a verification snippet.

IntegrationsUpdated

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:

HeaderValue
X-ServerStats-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-ServerStats-Eventalert.fired, alert.resolved, alert.test or report.weekly
X-ServerStats-DeliveryA 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

json
{"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:

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:

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 2xx is success. A 408, 429 or 5xx, a timeout or a network error is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours (a numeric Retry-After is 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.