Event webhooks
Event webhooks let another system react to what happens on your Moss instance: post to a chat bot when a server crashes, trigger a script when a backup finishes, feed a dashboard. Moss sends an HTTPS POST with a signed JSON body to each address you register. (The notification webhook in Settings, Notifications is separate: it sends one human-readable alert to a chat channel. Event webhooks are for programs.)
Set them up in Settings, Webhooks (administrators only). Each webhook has a name, an HTTPS address, the events it wants, and optionally the servers it is limited to. When you add one, Moss shows its signing secret once. Copy it into your receiver straight away; if you lose it, use New secret and update the receiver.
Events
| Event | When |
|---|---|
server.started |
A server finished starting and is ready |
server.stopped |
A server stopped |
server.crashed |
A server’s process died unexpectedly (data.detail has the reason) |
server.restarted |
A server became ready again within two minutes of going down, whether you restarted it or Moss did after a crash |
backup.completed |
A backup finished (data.backup has id, size, checksum) |
backup.failed |
A backup, or its copy to a remote destination, failed |
operation.completed / operation.failed |
Any long-running job (installs, updates, restores, …) ended; data.operation has its id, kind, title and error |
player.joined / player.left |
A player appeared in or vanished from the server’s player list |
content.installed |
A mod, plugin or modpack was installed |
update.available |
A content update check found newer versions (data.updates), or Moss raised an “update available” notification |
health.critical |
A new critical finding appeared on the Health page (checked once a minute) |
Limits worth knowing:
- Player events are derived from the server’s status response, which Moss polls every 5 seconds. A player who joins and leaves between two polls is not reported. Minecraft only lists up to 12 names, so on a busy server some joins and leaves are not reported, and servers that hide their player list send no player events. Nobody is reported as joining just because the server came back up.
- A server that was already running when Moss itself started does not produce
server.started. server.restartedreplacesserver.startedwhen the start follows a stop or crash closely; a plain stop before a restart still sendsserver.stopped.- Delivery is at least once. Retries and a Moss restart can deliver an event twice: de-duplicate on the event
id. Events for different endpoints, and different events, are not guaranteed to arrive in order. - Events that are not tied to a server (
update.availablefrom a notification, host-widehealth.critical) are only sent to webhooks that are not limited to specific servers.
What Moss sends
POST /your/path HTTP/1.1
Content-Type: application/json
User-Agent: Moss/0.2.0
X-Moss-Event: server.crashed
X-Moss-Delivery: 0194f3a2-... (same value on every retry of this delivery)
X-Moss-Timestamp: 1767225600 (Unix seconds, set on every attempt)
X-Moss-Signature: sha256=3b1f... (see below)
{"id":"0194f3a2-...","type":"server.crashed","time":"2026-01-01T00:00:00.000Z",
"server":{"id":"...","name":"Survival"},"data":{"detail":"exit code 1"}}
server is null for events that do not belong to one server. data is capped so a body never exceeds 64 KiB. Your receiver should answer with any 2xx status within 10 seconds; the answer body is not needed (Moss keeps the first 2 KB for the delivery log).
Verifying the signature
The signature is sha256= followed by the hex HMAC-SHA256 of the string <timestamp>.<raw body>, keyed with your webhook’s secret. Compute it over the raw bytes you received (not a re-serialised JSON object), compare in constant time, and reject requests whose timestamp is more than a few minutes old to stop replays.
Node.js (Express-style, using the raw body):
import crypto from 'node:crypto';
export function verify(secret, headers, rawBody, toleranceSeconds = 300) {
const ts = headers['x-moss-timestamp'];
const given = headers['x-moss-signature'] ?? '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSeconds) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(given);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// app.post('/hook', express.raw({ type: '*/*' }), (req, res) =>
// verify(SECRET, req.headers, req.body.toString('utf8')) ? res.sendStatus(204) : res.sendStatus(401));
Python:
import hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes, tolerance: int = 300) -> bool:
ts = headers.get("X-Moss-Timestamp", "")
given = headers.get("X-Moss-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, given)
Delivery, retries and the log
- Moss tries again after 5 s, 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (8 attempts in total). Any answer other than
2xxcounts as a failure, redirects are not followed, and an attempt that takes longer than 10 seconds is abandoned. - The queue is stored in Moss’s database, so pending deliveries survive a restart. A delivery that used all its attempts is marked gave up (dead letter) and stays in the log; use Send again to queue it once more.
- After 15 failed attempts in a row (counted across events; any success resets the count) Moss switches the webhook off and raises an administrator notification. Fix the receiver, then turn it back on.
- Each webhook shows its last 100 deliveries: state, attempts, HTTP status, latency and the first 2 KB of the response. Send test event sends a
testevent once, immediately, and is never retried or counted towards switching the webhook off.
Security notes
- Addresses must be
https://. Moss resolves the host once, refuses private, loopback, link-local and other non-public addresses, and connects to the address it checked. To deliver to a receiver on your own network, the administrator must start Moss withMOSS_ALLOW_PRIVATE_WEBHOOKS=true(this also applies to the notification webhook). - Secrets are stored in
webhook-secrets.jsonin the data directory (readable by the Moss user only) and are never returned by the API after creation, written to logs, or included in support bundles. The address is treated as sensitive too: it is not logged. - Anyone who has the secret can forge events, so give each receiver its own webhook.