Status · Guides
Receive webhooks
A webhook tells a program, rather than a person, that a monitor went down or came back: a pager, a failover, your own automation. Each one is a signed HTTPS request.
Add an endpoint
In the project's Settings, under Who gets told, choose Add endpoint on the Webhooks row. You need the admin role, and a sign-in at id.my-beast.com within the last 15 minutes, because the next page shows a secret.
- Address:
http://orhttps://, at a public address, with no user name or password in it. - Label (optional): what it's for, such as "Pager".
- Events:
monitor.down,monitor.up, or both.
Adding it shows its signing secret, which starts with whsec_, once. Store it with your other secrets: you need it to check that requests come from Status. A project has up to 10 endpoints.
The endpoint's own page has Send a test, which sends one status.test at once, without retries, and lists every delivery with each attempt: when, the status, how long it took and the start of the answer. An endpoint can't be changed: for a new address, events or secret, add a new one and remove the old one. Removing an endpoint stops what is still being retried.
What is sent, and when
| Event | Meaning |
|---|---|
monitor.down | A down, as Status tells it: an HTTP check failed twice in a row, a heartbeat is late, or its job reported a failure. |
monitor.up | Its recovery. |
status.test | Sent by Send a test, to see that the endpoint gets it and checks the signature. |
A down goes to every endpoint that wants it when Status tells people, even when no alert channel takes it, and so does its recovery. Nothing is sent for a down Status doesn't tell: one that came back within its wait, one that came back before the end of the maintenance window it began in, one while what it depends on is down, or one after the first while it's flapping. A down still going when its window ends is sent then. A flapping monitor's recovery sends nothing, and nor does a monitor paused or removed while down.
Each change is one message: the same webhook-id and body go to every endpoint, on every attempt.
Payload
Each message is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | MyBeast-Status/1.0 (+https://status.my-beast.com) |
webhook-id | The message's ID, like msg_01jbx3k9v2q8m7n6p5r4s3t2w1. The same on every attempt. |
webhook-timestamp | When this attempt was sent, in Unix seconds. |
webhook-signature | v1, and the signature: see Verify signatures. |
{
"type": "monitor.down",
"timestamp": "2026-11-01T12:00:45.120Z",
"data": {
"project": {
"slug": "shop",
"name": "Shop"
},
"monitor": {
"id": "9d1c4a7e-5b2f-4e8a-9c3d-2f6b8a1e7c40",
"key": "api",
"name": "API",
"kind": "http",
"severity": "sev1",
"environment": "production"
},
"state": "down",
"reason": "2 checks in a row failed: Answered 503",
"at": "2026-11-01T12:00:30.000Z",
"url": "https://services.my-beast.com/acme/shop/status/monitors/9d1c4a7e-5b2f-4e8a-9c3d-2f6b8a1e7c40"
}
}A monitor.up has "state": "up" and, when Status saw the down, down_for_seconds. Every field is in the API reference, under Webhook messages.
Verify signatures
Anyone can send a request to your endpoint, so check that each one comes from Status before you act on it. Status signs the Standard Webhooks way, so any library for it works. By hand:
- Take the raw request body, exactly as it arrived. Parsing the JSON and encoding it again changes the bytes, and the check fails.
- Join
webhook-id, a full stop,webhook-timestamp, a full stop and the body:<id>.<timestamp>.<body>. - Compute an HMAC-SHA256 of that. The key is the secret's bytes: the base64 after
whsec_, decoded. - Encode the result as base64 and compare it, in constant time, with the part after
v1,inwebhook-signature. - Check that
webhook-timestampis no more than 5 minutes away from your server's clock, so an old request can't be replayed.
If any step fails, answer with an error such as 400 and ignore the request.
import crypto from "node:crypto";
// rawBody: the request body exactly as it arrived, as a string or a Buffer.
export function verifyStatusWebhook(rawBody, headers, secret) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
if (!id || !/^\d+$/.test(timestamp ?? "") || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false;
}
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
return (headers["webhook-signature"] ?? "").split(" ").some((entry) => {
const [version, signature = ""] = entry.split(",");
const given = Buffer.from(signature, "base64");
return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}Keep your server's clock right
Responses and retries
Answer with any 2xx within 5 seconds to take the message. Anything else is a no: another status, a redirect (it isn't followed), a connection error, or no answer in time.
After a no, Status tries again 5 seconds, 30 seconds, 2 minutes, 5 minutes and 7 minutes later, each counted from the attempt before: 6 attempts over about 15 minutes. Then it gives up, because a late "down" is no use. The address is checked again before every attempt, and an address that is no longer public isn't called.
Good practice
- Answer first, work later. Store the message or put it on a queue, answer
200, then act on it. - Expect duplicates. A retry can bring a message you already handled. Its
webhook-ididentifies it. - Don't rely on order. After retries, a
monitor.upcan arrive before itsmonitor.down.data.atsays when each change happened. - Test before it matters. Send a test from the endpoint's page, and check that your code verifies its signature.