For a receiver of InBrief's outbound webhooks
Webhooks integration guide
Outbound webhooks capability
Creating a webhook
In the dashboard, go to Notifications → Webhooks and choose Add webhook. You give it an https URL, an optional description to tell several apart, and tick the events it should receive.
The signing secret is shown once, on the screen straight after you add it, and never again anywhere in the dashboard. Copy it then. If you lose it, remove the webhook and add a new one.
Before you rely on an endpoint, open the webhook and choose Send test. It delivers a
sample webhook.test payload immediately and records the result alongside everything else, so
you can confirm the endpoint is reachable and check your signature code against a real request.
Events
A webhook receives only the events you subscribe it to. Click the webhook's URL on the Webhooks page to change the selection at any time.
| Event | Fires when | Requires |
|---|---|---|
incident.created | An incident is posted | Incident management |
incident.updated | An existing incident is edited | Incident management |
incident.resolved | An incident is marked resolved | Incident management |
postmortem.published | An incident postmortem is published | Incident postmortems |
maintenance.created | A maintenance window is scheduled | Incident management |
maintenance.updated | A scheduled window is edited | Incident management |
maintenance.resolved | A maintenance window is marked complete | Incident management |
monitor.down | A monitor fails enough consecutive checks to count as down | Monitoring |
monitor.recovered | A monitor that was down starts passing again | Monitoring |
ssl.expiring | A monitored certificate crosses an expiry threshold | Monitoring |
domain.expiring | A watched domain registration crosses an expiry threshold | Monitoring and an enabled domain-registration rollout |
Monitor, certificate, and domain-registration events require monitoring. Incident and maintenance events require incident management. The Webhooks screen only offers event families the account can produce.
Domain-registration delivery is paused by default. Selecting domain.expiring does not
enable its rollout or guarantee that warnings will be delivered.
New event types are always opt-in. Adding one to this list never changes what an existing webhook receives, so a receiver written today keeps getting exactly what it gets today.
Payload
Every delivery is a POST with content-type: application/json. Four fields are on
every payload, whatever the event:
| Field | What it is |
|---|---|
id | Unique per delivery. Use it as your idempotency key. |
timestamp | ISO 8601 UTC instant the delivery was built. |
event | One of the event names above. |
tenant | { slug, name } of the status page it came from. |
What comes after those depends on the event:
| Event | Also carries |
|---|---|
incident.*, maintenance.* |
incident: { id, type, title, summary, startedAt, resolvedAt }. Lifecycle-managed maintenance also carries publishedAt, scheduledEndAt, maintenanceState, and cancelledAt. |
postmortem.published |
incident: { id, title }, plus postmortem: { excerpt, publishedAt, url } |
monitor.down, monitor.recovered |
monitor: { id, name, url } |
ssl.expiring |
monitor, plus certificate: { expiresAt, daysRemaining } |
domain.expiring |
domain: { registrableDomain, expirySource, expiresAt, daysRemaining, thresholdDays, monitorCount } |
webhook.test | message: a fixed human-readable string |
A domain.expiring delivery carries no monitor object: one registration covers
every monitor on that domain, and monitorCount says how many. expiresAt is the
registry's date, and daysRemaining can be negative once it has passed.
An incident.created delivery in full:
{
"id": "delivery_example_01",
"timestamp": "2026-08-16T09:00:00.000Z",
"event": "incident.created",
"tenant": {
"slug": "acme",
"name": "Acme"
},
"incident": {
"id": "inc_example_02",
"type": "incident",
"title": "Elevated error rates",
"summary": "We are investigating reports of elevated error rates on the API.",
"startedAt": "2026-08-15T09:00:00.000Z",
"resolvedAt": null
}
}title and summary are always in the status page's default language. A
webhook receiver is a system, not a person choosing a language, so there is no per-recipient localisation
the way subscriber emails have. resolvedAt stays null until the incident is
actually marked resolved.
Verifying a delivery
Every request carries a signature header:
X-Inbrief-Signature: sha256=<hex><hex> is the lowercase-hex HMAC-SHA256 of the raw request body (the
exact bytes received, before any JSON parsing), keyed with that webhook's own secret. Recompute it and
compare with a constant-time comparison, never == or ===, before trusting
anything in the payload.
In Node.js:
const crypto = require('node:crypto');
function isValidSignature(rawBody, header, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const given = (header || '').replace(/^sha256=/, '');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(given, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Any language with an HMAC-SHA256 primitive and a constant-time byte compare works the same way. The recipe is just "HMAC the raw body with your secret, hex-encode it, compare to the header".
Delivery semantics
| Behaviour | Detail |
|---|---|
| Timeout | 15 seconds per attempt. |
| Retry | One retry after a short delay, on a timeout, a connection failure, or any non-2xx status. Nothing beyond that. |
| Success | Any 2xx. Return one as soon as you have accepted the payload; do the slow work afterwards. |
| Ordering | Not guaranteed. Two events fired close together can arrive in either order. |
| Duplicates | Possible. Deduplicate on id. |
| Response body | Read up to 2000 characters and stored for you to inspect. Anything longer is discarded unread. |
Deduplicate on id, not on the incident id, because the same incident legitimately produces many
incident.updated events, and each is a distinct delivery you are meant to process.
Delivery history
Every attempt is recorded. Open Delivery history on a webhook to see the most recent 50: the event, the response status, how long it took, and how many attempts it needed.
Opening one shows the exact request body that was signed and sent, and whatever the receiver sent back, or, when nothing came back at all, the reason: a timeout, a DNS failure, a refused connection.
Send this again on a delivery replays those exact bytes, keeping the same id,
so a receiver that already processed it can recognise the duplicate. The replay is recorded as its own
delivery rather than overwriting the original, so a success after a failure stays visible as both.
History is kept for 30 days. Removing a webhook removes its history too.