Webhook events
Webhooks in the JSON format are a POST with this body. Setting them up: Webhooks.
{ "id": "…", "event": "server.state_changed", "createdAt": "2026-10-11T12:00:00Z", "tenantId": "…", "server": { "id": "…", "name": "DayZ" }, "data": { "state": "offline" }}| Event | data |
|---|---|
server.state_changed |
{ "state" }: only running, offline or installFailed, sent once the state has held for 15 seconds and differs from the last one sent. For every change, use the console stream. |
server.created |
{ "template", "node" } |
server.deleted |
{} |
server.install_finished |
{ "succeeded", "exitCode" } |
ping |
sent by Test |
Headers
Section titled “Headers”X-Panel-Event: the eventX-Panel-Delivery: a unique id; use it to de-duplicate retriesX-Panel-Signature:t=1760184000,v1=<hex HMAC-SHA256(secret, "1760184000." + raw body)>
Verify the signature
Section titled “Verify the signature”- Recompute the HMAC over
t + "." + body, using the raw bytes as received. - Compare in constant time.
- Reject timestamps older than a few minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) { 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}.`).update(rawBody).digest(); const given = Buffer.from(v1, 'hex'); return given.length === expected.length && timingSafeEqual(given, expected);}Delivery
Section titled “Delivery”- Any
2xxanswer counts as delivered. - Otherwise the panel retries after 1, 2, 4… up to 60 minutes, 8 attempts in total.
- Redirects are not followed.