Skip to content

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

  1. Create a check per worker you care about. Most users start with one per host for backup; add check / replicate later.
  2. Configure the schedule on the check side to match BACKUP_CRON.
  3. Set WEBHOOK_URL to the https://hc-ping.com/<uuid> URL.
  4. Leave WEBHOOK_ON_ERROR=OFF (default) so healthchecks knows the run happened on time even when it succeeds.

hc-ping.com interprets:

  • An HTTP 200 POST as success.
  • A POST to <uuid>/fail as fail — the helper does not flip the URL for you; if you only want failure pings, use the /fail form directly and combine with WEBHOOK_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 in cron.log or in last-<job>.json.
  • The WEBHOOK_HEADER_AUTH value 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.