Webhooks¶
When WEBHOOK_URL is set, every worker POSTs the same JSON document as
/var/log/last-<job>.json to that endpoint after each run. The webhook
sink is additive with the file sink — pick either or both depending
on your monitoring stack.
Quick configuration¶
environment:
WEBHOOK_URL: https://hc-ping.com/00000000-0000-0000-0000-000000000000
WEBHOOK_HEADER_AUTH: "Bearer hunter2" # optional
WEBHOOK_TIMEOUT: "15" # default 10s
WEBHOOK_ON_ERROR: "OFF" # default; ON = only on failure
| Variable | Default | Behaviour |
|---|---|---|
WEBHOOK_URL unset |
– | No-op; nothing is posted. |
WEBHOOK_URL set, WEBHOOK_ON_ERROR=OFF (default) |
– | POST after every run regardless of exit code. |
WEBHOOK_URL set, WEBHOOK_ON_ERROR=ON |
– | POST only when the job exited with a non-zero code. |
WEBHOOK_HEADER_AUTH set |
– | Added as Authorization: <value> (Bearer …, Token …, …). Value is never echoed to logs. |
WEBHOOK_TIMEOUT (default 10) |
– | Curl --max-time in seconds; a hung endpoint cannot block a backup. |
Webhooks never fail the worker
Failures (curl non-zero exit, HTTP non-2xx, timeout) are logged as errors but never propagate to the worker's exit code. A flaky webhook endpoint cannot turn an otherwise-successful backup into a failed one.
What is in the body¶
The POST body is the same JSON written to /var/log/last-<job>.json. Per
worker, the fields are listed in JSON summaries;
the common subset every worker always emits is:
{
"job": "backup",
"hostname": "backup-node",
"release": "2.2.2-0.18.1",
"started_at": "2026-05-11T02:00:00+0200",
"finished_at": "2026-05-11T02:05:12+0200",
"started_epoch": 1762828800,
"finished_epoch": 1762829112,
"duration_seconds": 312,
"exit_code": 0,
"repository": "s3:https://s3.example.com/***@bucket/restic"
}
The repository field is masked (mask_repository) — userinfo
between :// and @ becomes *** before printing, posting or mailing.
Configured rclone: remotes hide their credentials in rclone.conf and
never appear in the URL at all.
Compatible endpoints¶
Tested out of the box with:
| Endpoint | URL pattern | Notes |
|---|---|---|
| healthchecks.io | https://hc-ping.com/<uuid> |
Body is logged but not parsed; healthchecks only cares about HTTP status and timing. Set one check per worker if you want separate alerts. |
| Slack incoming webhook | https://hooks.slack.com/services/T…/B…/… |
Slack expects {"text": "…"} — point at a small bridge (e.g. an Apprise endpoint) if you want the full JSON parsed, or write a pre/post hook that calls Slack with a custom body. |
| Discord incoming webhook | https://discord.com/api/webhooks/… |
Same caveat as Slack. |
| Mattermost incoming webhook | https://mattermost.example.com/hooks/… |
Same caveat. |
| Gotify | https://gotify.example.com/message?token=… |
Accepts the JSON as-is via Gotify's plugin filter, or wrap in a hook. |
| ntfy | https://ntfy.example.com/<topic> |
Supports JSON publishing via custom headers; consider a small hook for rich formatting. |
| Apprise receivers | http://apprise.example.com/notify/<key> |
Apprise translates the JSON to whichever channel its config defines. |
| Custom HTTPS endpoint | Anything that accepts Content-Type: application/json POST |
Most flexible; you control the schema. |
The helper's webhook stack is stateless — every run produces a self-contained document. There is no retry queue: if the endpoint is down at the moment of POST, the run is logged as a webhook failure and the next cron tick posts the next document.
Healthchecks.io recipe¶
- Create a check per worker you care about. Most users start with one
per host for
backup; addcheck/replicatelater. - Configure the schedule on the check side to match
BACKUP_CRON. - Set
WEBHOOK_URLto thehttps://hc-ping.com/<uuid>URL. - Leave
WEBHOOK_ON_ERROR=OFF(default) so healthchecks knows the run happened on time even when it succeeds.
hc-ping.com interprets:
- An HTTP
200POST assuccess. - A POST to
<uuid>/failasfail— the helper does not flip the URL for you; if you only want failure pings, use the/failform directly and combine withWEBHOOK_ON_ERROR=ON.
Slack / Discord with a wrapping hook¶
Slack/Discord/Mattermost incoming webhooks want their own envelopes
({"text": "…"}). The simplest pattern is to keep the helper's webhook
pointed at an HTTP collector (or leave it unset) and emit Slack messages
from a /hooks/post-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
rc="${1:-0}"
[ "${rc}" -eq 0 ] && exit 0
[ -n "${SLACK_WEBHOOK_URL:-}" ] || exit 0
text=$(cat /var/log/last-backup.json | jq -r '"\(.hostname) backup failed (rc=\(.exit_code)) after \(.duration_seconds)s"')
curl -fsS -m 10 -H 'Content-Type: application/json' \
-d "$(jq -nc --arg t "${text}" '{text:$t}')" "${SLACK_WEBHOOK_URL}"
Combine with WEBHOOK_URL pointing at healthchecks.io for the green
heartbeat, and the hook for the red escalation.
Privacy¶
- The webhook URL is logged as
scheme://host/…only — per-recipient secrets in path/query (healthchecks UUIDs, Slack/Discord tokens, ntfy topics) never appear incron.logor inlast-<job>.json. - The
WEBHOOK_HEADER_AUTHvalue is never echoed; logs only mention "auth header set". - The POST body itself can contain sensitive metadata (paths, hostname, release). If your endpoint is shared with third parties, treat the payload as operationally sensitive.
See also¶
- JSON summaries — what is in each webhook body.
- Mail notifications — push-based, human-readable alternative.
- Hooks — fan-out to channel-specific formats from a single source of truth.