# Notifications (/docs/notifications)

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 [#add-a-channel]

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

| Channel | What you need |
| --- | --- |
| **Email** | **Recipients**: one or more addresses, separated by commas. |
| **Slack** | **Webhook URL** from an incoming webhook: Slack app → Incoming Webhooks → Add New Webhook to Workspace. |
| **Discord** | **Webhook URL**: Channel settings → Integrations → Webhooks → Copy Webhook URL. |
| **Microsoft Teams** | **Workflow webhook URL**: in the channel, Workflows → "Post to a channel when a webhook request is received". |
| **Telegram** | **Bot 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 webhook** | **Endpoint URL**: any HTTPS endpoint. backupd sends signed JSON. |

## Events [#events]

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

| Event | Category | Default | When |
| --- | --- | --- | --- |
| **Backup failed** | Backups | On | A scheduled or manual backup did not complete. |
| **Copy to destination failed** | Backups | On | A copy to one of your own buckets failed. |
| **Restore test failed** | Backups | On | A restore test against a temporary database failed. |
| **Database unreachable** | Backups | On | An uptime check couldn't connect to a database. |
| **Backup succeeded** | Backups | Off | Every successful backup. Noisy on hourly schedules. |
| **Storage at 80%** | Billing | On | 80% of the plan's included storage is used. |
| **Storage at 100%** | Billing | On | All of the plan's included storage is used. |
| **Spending cap reached** | Billing | On | Extra storage reached your spending cap, and new backups are paused. |
| **Plan changed** | Billing | Off | The workspace plan was upgraded, downgraded or cancelled. |
| **Member joined** | Workspace | Off | Someone 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 [#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 [#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 [#webhook-payloads]

A **Custom webhook** receives a `POST` with a JSON body:

```json
{
  "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:

| Header                | Value                                             |
| --------------------- | ------------------------------------------------- |
| `x-backupd-event`     | The event type, for example `backup.failed`.      |
| `x-backupd-event-id`  | The event's ID. Use it to ignore duplicates.      |
| `x-backupd-timestamp` | Unix time in seconds when the request was signed. |
| `x-backupd-signature` | `t=<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 [#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:

```js
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.
