Skip to documentation
InBrief

Webhooks integration guide

For a receiver of InBrief's outbound webhooks

Outbound webhooks capability

On this page6

Creating a webhook

In the dashboard, go to Alerts → Channels and choose Add channel → 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.

EventFires whenRequires
incident.createdAn incident is postedIncident management
incident.updatedAn existing incident is editedIncident management
incident.resolvedAn incident is marked resolvedIncident management
postmortem.publishedAn incident postmortem is publishedIncident postmortems
maintenance.createdA maintenance window is scheduledIncident management
maintenance.updatedA scheduled window is editedIncident management
maintenance.resolvedA maintenance window is marked completeIncident management
monitor.downA monitor fails enough consecutive checks to count as downMonitoring
monitor.recoveredA monitor that was down starts passing againMonitoring
ssl.expiringA monitored certificate crosses an expiry thresholdMonitoring
domain.expiringA watched domain registration crosses an expiry thresholdMonitoring; domain-registration warnings are coming soon

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 warnings are coming soon. You can select domain.expiring now, but no warnings are delivered yet.

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.

Internal alert events

Two more events are not in the list above, because a webhook does not subscribe to them. An Internal alert rule names the webhook it reports to, and the rule decides when they are sent:

EventFires whenRequires
internal.alert.triggeredThe rule's condition starts to match what your servers reportInternal analytics, or Database Health for a database rule
internal.alert.resolvedThe condition stops matching, or the rule is edited or deleted while it matchesThe same as the rule

They are signed and delivered like every other event. A paused webhook, a disabled rule and a muted rule send nothing. A failed delivery is retried with the same id, so deduplicate on it as usual.

Payload

Every delivery is a POST with content-type: application/json. Four fields are on every payload, whatever the event:

FieldWhat it is
idUnique per delivery. Use it as your idempotency key.
timestampISO 8601 UTC time of the event: when the change was saved or the check that found it ran. Every redelivery of the event carries the same value.
eventOne of the event names above.
tenant{ slug, name } of the status page it came from.

What comes after those depends on the event:

EventAlso 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 }
internal.alert.* alert: { id, ruleId, state, summary, snapshotRevision, fingerprint, affectedReporters, affectedRequests, affectedErrors, affectedErrorRate, dominantStatus, onsetAt, resolvedAt }
webhook.testmessage: a fixed human-readable string

In an internal.alert.* delivery, state is triggered or resolved and summary is a sentence you can post as it is. Every other field in alert can be null. affectedErrorRate is a fraction (errors divided by requests), dominantStatus is the error status seen most often, and onsetAt and resolvedAt are ISO 8601 instants.

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:

Response
{
  "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:

Example
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:

Example
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".

Each request also carries a second header that signs the time of the attempt, so a captured delivery stops verifying once you decide it is too old:

Example
X-InBrief-Signature-V1: t=<unix seconds>,v1=<hex>

<hex> is the HMAC-SHA256, keyed with the same secret, of the string <t>.<raw body>. Recompute it, compare in constant time, and reject the request when t is more than five minutes from your clock. A redelivery from the history screen carries the same body and the same id but a new t. The body-only header above keeps working for now; move to this one when you can.

Delivery semantics

BehaviourDetail
Timeout15 seconds per attempt.
RetryA timeout, a connection failure or any non-2xx status is retried once, half a second later. If that fails too, the event is redelivered later with growing gaps, from about 30 seconds up to 15 minutes, for at most 8 attempts and never more than 24 hours after the event. Only receivers that have not yet answered with a 2xx are called again. ssl.expiring gets the quick retry only.
SuccessAny 2xx. Return one as soon as you have accepted the payload; do the slow work afterwards.
Repeated failureAfter ten deliveries in a row without a 2xx, redeliveries included, the webhook is paused so a receiver that no longer exists stops being called. Resume it from the list once the receiver answers again.
OrderingNot guaranteed. Two events fired close together can arrive in either order.
DuplicatesPossible: every redelivery of an event keeps the same id. Deduplicate on it.
Response bodyRead 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.