backupd

Notifications

Get backupd alerts for failed backups, failed restore tests, unreachable databases and storage limits on Slack, Discord, Microsoft Teams, Telegram, email or a signed webhook.

Notification channels tell you when something needs attention. Open Notifications (or Settings → Notifications) to manage them. Owners and admins can add, edit and remove channels. Everyone in the workspace can see them and their delivery log.

Add a channel

Click Add channel, choose a type, give it a Name, fill in its fields, and pick the events it should receive.

ChannelWhat you need
EmailRecipients: one or more addresses, separated by commas.
SlackWebhook URL from an incoming webhook: Slack app → Incoming Webhooks → Add New Webhook to Workspace.
DiscordWebhook URL: Channel settings → Integrations → Webhooks → Copy Webhook URL.
Microsoft TeamsWorkflow webhook URL: in the channel, Workflows → "Post to a channel when a webhook request is received".
TelegramBot token from @BotFather, and the Chat ID: a numeric chat ID, or @channelname for a public channel. Add the bot to the chat first.
Custom webhookEndpoint URL: any HTTPS endpoint. backupd sends signed JSON.

Events

Events marked "on" are selected by default for a new channel.

EventCategoryDefaultWhen
Backup failedBackupsOnA scheduled or manual backup did not complete.
Copy to destination failedBackupsOnA copy to one of your own buckets failed.
Restore test failedBackupsOnA restore test against a temporary database failed.
Database unreachableBackupsOnAn uptime check couldn't connect to a database.
Backup succeededBackupsOffEvery successful backup. Noisy on hourly schedules.
Storage at 80%BillingOn80% of the plan's included storage is used.
Storage at 100%BillingOnAll of the plan's included storage is used.
Spending cap reachedBillingOnExtra storage reached your spending cap, and new backups are paused.
Plan changedBillingOffThe workspace plan was upgraded, downgraded or cancelled.
Member joinedWorkspaceOffSomeone accepted an invitation.

Billing alerts at 80% and 100% of included storage are also emailed to the workspace's owners and admins, whether or not you have set up a channel.

Manage channels

From the ⋯ menu next to a channel:

  • Send test sends a Test notification to check the channel works.
  • Edit events changes what the channel receives.
  • Pause or enable the channel without deleting it.
  • Remove the channel.

Delivery log

The Delivery log shows the last 30 deliveries and whether each was Delivered, Queued or Failed (with the number of attempts). Failed deliveries are retried with increasing delays for up to about an hour. A delivery that fails because the URL is wrong or the webhook was revoked is not retried. Fix the channel and send a test.

Webhook payloads

A Custom webhook receives a POST with a JSON body:

{
  "id": "4f0c2a9e-…",
  "type": "backup.failed",
  "createdAt": "2026-10-16T09:00:12.000Z",
  "organization": { "id": "…", "name": "Acme", "slug": "acme" },
  "data": { "…": "…" },
  "message": {
    "title": "…",
    "summary": "…",
    "url": "https://…"
  }
}

Each request has these headers:

HeaderValue
x-backupd-eventThe event type, for example backup.failed.
x-backupd-event-idThe event's ID. Use it to ignore duplicates.
x-backupd-timestampUnix time in seconds when the request was signed.
x-backupd-signaturet=<timestamp>,v1=<signature>

Event types: backup.failed, backup.replication_failed, backup.verification_failed, backup.succeeded, database.unreachable, storage.threshold_80, storage.threshold_100, budget.cap_reached, subscription.changed, member.joined, and test for Send test.

Verify the signature

When you add a webhook channel, backupd shows a signing secret once. Save it. The signature is the hex HMAC-SHA256 of the timestamp, a dot, and the raw request body, using the signing secret:

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

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Reject requests whose timestamp is too old to protect against replays.

On this page