Skip to content
Documentation menu

Webhooks

Signed events to n8n, Make, Zapier or your own tools when an alert opens, changes or recovers.

Last updated

Webhooks send an HTTP request to your own endpoint when an alert opens, changes or recovers, so n8n, Make, Zapier, an internal tool or a script can act on it. Every request is signed, retried when your endpoint is down, and recorded in a delivery log.

Webhooks are on every plan: 1 endpoint on Starter, 5 on Growth (and during the free trial), 25 on Pro.

Add an endpoint

Owners and admins add endpoints in Settings → Integrations → Webhooks:

  1. Choose Add endpoint. Give it a name, the URL to call and, optionally, a description.
  2. Choose the events it receives (see below).
  3. Choose Add endpoint. The signing secret is shown once: copy it into your endpoint's configuration. It is stored encrypted and cannot be shown again; Rotate secret replaces it.
  4. Choose Send test to send a webhook.test event. The answer appears straight away: the HTTP status, the time it took and the start of the response.

The URL must start with https:// and reach the public internet on port 443 or 8443. Addresses on private networks, loopback, link-local and cloud metadata addresses are refused, when the URL is saved and again for every request.

Events

Each change of an alert is one event. An alert is one incident: a backup fails and the alert opens; further failures update that same alert until the backup recovers.

EventSent when
alert.createdA backup failed, sent a warning, sent no report in time (missing) or failed an integrity check, and no alert was open for it.
alert.updatedThe open alert got worse: the backup failed again (the failure streak grew), or a warning turned into a failure.
alert.acknowledgedSomeone acknowledged the alert.
alert.resolvedThe backup recovered, or someone resolved the alert.
alert.reopenedSomeone reopened a resolved alert.
backup.failed, backup.warning, backup.missingThe same moment as alert.created, named by what happened to the backup.
backup.recoveredThe same moment as alert.resolved, when it resolved because the backup succeeded.

The backup.* events are other names for the same changes. An endpoint never receives two requests for one change: subscribed to both alert.created and backup.failed, it receives alert.created once. A new report email for a backup that is already failing sends alert.updated, never a second alert.created.

Payload

Every request is a POST with a JSON body:

{
  "id": "evt_8f2c1d9e0a7b4c3d9e8f7a6b5c4d3e2f",
  "type": "alert.created",
  "incidentEvent": "alert.created",
  "createdAt": "2026-10-17T02:04:31.000Z",
  "workspace": { "id": "5b0c…", "name": "Atlas IT" },
  "client": { "id": "8a0f…", "name": "Pharos Advisory" },
  "job": { "id": "3c1e…", "name": "Microsoft 365", "vendor": "synology", "vendorName": "Synology DSM", "lastSuccessAt": "2026-10-16T02:01:00.000Z" },
  "alert": {
    "id": "d4a7…",
    "type": "failed",
    "title": "Backup failed",
    "status": "open",
    "severity": "critical",
    "summary": "Microsoft 365 backup failed",
    "createdAt": "2026-10-17T02:04:31.000Z",
    "acknowledgedAt": null,
    "resolvedAt": null,
    "failureStreak": 1,
    "url": "https://app.backupsentinel.io/dashboard/jobs/3c1e…"
  },
  "change": null,
  "ai": { "likelyCause": "The Microsoft 365 authorisation expired.", "confidence": "high", "suggestedChecks": ["Re-authorise the tenant", "Retry the backup"] }
}
  • type is the event the endpoint subscribed to; incidentEvent is always the alert.* name.
  • id identifies the event and stays the same on every retry. Use it to ignore a request you already processed.
  • alert.type is failed, missing, warning or integrity; alert.status is open, acknowledged, snoozed or resolved.
  • change says what changed for alert.updated ({"reason":"failed_again","failureStreak":3} or {"reason":"severity","from":"warning","to":"critical"}) and for alert.resolved ({"resolution":"recovered"} or "manual").
  • ai is BackupSentinel's AI explanation when one exists: a suggestion, not a diagnosis. null otherwise.
  • summary and job names can quote the backup software's report: treat them as data.
  • Never sent: report email bodies, subjects or senders, ingest addresses, notes, contacts, AI prompts, credentials or secrets.

A test send has "type": "webhook.test", "test": true and no alert.

Verify signatures

Every request carries these headers:

HeaderValue
X-BackupSentinel-EventThe event type, as in the body.
X-BackupSentinel-DeliveryThe delivery id, the same on every retry of it.
X-BackupSentinel-TimestampUnix seconds when this attempt was sent.
X-BackupSentinel-Signaturev1= followed by the hex HMAC-SHA256 of timestamp + "." + raw body, keyed with the signing secret.

To verify a request:

  1. Read the raw body exactly as received, before parsing the JSON.
  2. Compute HMAC_SHA256(secret, timestamp + "." + rawBody) as hex, add v1=, and compare it with the header in constant time.
  3. Reject the request if the timestamp is more than 5 minutes from your clock. This stops a captured request from being replayed later.
  4. Ignore an id you have already processed.

In Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(req, rawBody, secret) {
  const ts = req.headers["x-backupsentinel-timestamp"];
  const sig = req.headers["x-backupsentinel-signature"] ?? "";
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "v1=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

In Python:

import hmac, hashlib, time

def verify(headers, raw_body: bytes, secret: str) -> bool:
    ts = headers.get("X-BackupSentinel-Timestamp", "")
    sig = headers.get("X-BackupSentinel-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    expected = "v1=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

Responses, retries and timeouts

  • Answer with any 2xx status within 10 seconds. The body of your answer is ignored.
  • Anything else, a timeout or a connection error is retried: after 1 minute, 5 minutes, 30 minutes and 2 hours. A Retry-After header is honoured. After the fifth attempt the delivery is marked failed.
  • Redirects are not followed: give the final URL.
  • Deliveries are sent in the background, so a slow endpoint never delays monitoring. Events of one alert usually arrive in order, but are not guaranteed to: compare createdAt if order matters.
  • An endpoint whose deliveries fail 25 times in a row is paused. Settings shows why; Resume starts sending again.

Delivery log

Deliveries under each endpoint lists the latest deliveries: the event, the result, the HTTP status and response time of the last attempt, the number of attempts and when. Payload shows the exact JSON that was sent. Retry sends a failed delivery again. Only the first 500 characters of a response are kept, with anything that looks like a token or key removed.

Integration health at the top of Settings → Integrations shows every endpoint with problems first: failed deliveries in the last 24 hours, retries, or a pause.

Limits

  • Webhooks only send. They cannot change anything in BackupSentinel; use the MCP server with a Read and act key for that.
  • Snoozing an alert, assigning it and taking back an acknowledgement do not send events yet.
  • Requests come from BackupSentinel's cloud servers, whose addresses change: a firewall cannot allow-list a fixed IP. Verify the signature instead.
  • Delivery history is kept for 90 days.