Webhooks
Send alerts to Slack, a Discord channel, or your own tools, without adding our Discord bot. You get a message when something goes down or comes back, and your incident updates.
- In your project, open Destinations and choose Add a webhook.
- Pick where alerts should go, and paste the webhook URL from that service.
- Check the preview and save. A test message is sent straight away.
Where to find the URL
| Slack | Create a Slack app, turn on Incoming Webhooks, add it to a channel, and copy the webhook URL. Mattermost and Rocket.Chat incoming webhooks work the same way. |
|---|---|
| Discord webhook | Open the channel's settings, then Integrations, then Webhooks, and copy a webhook URL. Messages look the same as the bot's, but there is no live status message, because a webhook can't edit its posts. |
| JSON | For your own code, or tools like Zapier, n8n, Make, or ntfy. Each request is signed so you can check it came from us. |
What we send
One HTTPS POST per alert, with a JSON body. JSON webhooks receive this shape.type is one of monitor.down, monitor.up, monitor.shards_down, monitor.shards_up, certificate.expiring, incident.opened, incident.updated, incident.resolved, maintenance.scheduled, maintenance.started, maintenance.ended, or test.
{
"id": "6713f0c2a9…",
"type": "monitor.down",
"title": "Website is offline",
"description": "Checks failed from two locations: the request timed out.",
"status": "major_outage",
"url": "https://statusbeat.app/s/acme/incidents/inc_…",
"lines": [],
"fields": [{ "name": "Offline since", "value": "3 Oct 2026, 14:05 UTC" }],
"occurredAt": "2026-10-03T14:05:00.000Z"
}Checking the signature
When you add a JSON webhook you get a signing secret, shown once. Every request carries a statusbeat-signature header like t=1791036300,v1=5f2a…, where v1 is an HMAC-SHA256 of t, a dot, and the raw body. Reject anything that doesn't match or is more than five minutes old.
import { createHmac, timingSafeEqual } from 'node:crypto';
// `header` is the statusbeat-signature header, `body` the raw request body as a string.
export function isFromStatusbeat(header, body, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${body}`).digest('hex');
if (expected.length !== parts.v1.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}If something goes wrong
- Errors and timeouts are retried for a while, with growing gaps between attempts.
- If your endpoint says to slow down (HTTP 429), we wait as long as it asks.
- If the URL stops existing (HTTP 404 or 410), the webhook is paused and your dashboard tells you. Add it again with a working URL.
- We never follow redirects, and never send to private or internal addresses.
Prefer a live status message that updates itself? Use the Discord bot instead.