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:
- Choose Add endpoint. Give it a name, the URL to call and, optionally, a description.
- Choose the events it receives (see below).
- 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.
- Choose Send test to send a
webhook.testevent. 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.
| Event | Sent when |
|---|---|
alert.created | A backup failed, sent a warning, sent no report in time (missing) or failed an integrity check, and no alert was open for it. |
alert.updated | The open alert got worse: the backup failed again (the failure streak grew), or a warning turned into a failure. |
alert.acknowledged | Someone acknowledged the alert. |
alert.resolved | The backup recovered, or someone resolved the alert. |
alert.reopened | Someone reopened a resolved alert. |
backup.failed, backup.warning, backup.missing | The same moment as alert.created, named by what happened to the backup. |
backup.recovered | The 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"] }
}
typeis the event the endpoint subscribed to;incidentEventis always thealert.*name.ididentifies the event and stays the same on every retry. Use it to ignore a request you already processed.alert.typeisfailed,missing,warningorintegrity;alert.statusisopen,acknowledged,snoozedorresolved.changesays what changed foralert.updated({"reason":"failed_again","failureStreak":3}or{"reason":"severity","from":"warning","to":"critical"}) and foralert.resolved({"resolution":"recovered"}or"manual").aiis BackupSentinel's AI explanation when one exists: a suggestion, not a diagnosis.nullotherwise.summaryand 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:
| Header | Value |
|---|---|
X-BackupSentinel-Event | The event type, as in the body. |
X-BackupSentinel-Delivery | The delivery id, the same on every retry of it. |
X-BackupSentinel-Timestamp | Unix seconds when this attempt was sent. |
X-BackupSentinel-Signature | v1= followed by the hex HMAC-SHA256 of timestamp + "." + raw body, keyed with the signing secret. |
To verify a request:
- Read the raw body exactly as received, before parsing the JSON.
- Compute
HMAC_SHA256(secret, timestamp + "." + rawBody)as hex, addv1=, and compare it with the header in constant time. - Reject the request if the timestamp is more than 5 minutes from your clock. This stops a captured request from being replayed later.
- Ignore an
idyou 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-Afterheader 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
createdAtif 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.