diff --git a/README.md b/README.md
index 88aecf0b..37999202 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,7 @@ Zerobyte can be customized using environment variables. Below are the available
| `TRUST_PROXY` | When `true`, trust an existing `X-Forwarded-For` header from your reverse proxy. Leave `false` for direct deployments. | `false` |
| `TRUSTED_ORIGINS` | Comma-separated list of extra trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) |
| `WEBHOOK_ALLOWED_ORIGINS` | Comma-separated list of HTTP origins allowed for backup webhooks and outbound HTTP notification destinations. | (none) |
+| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds. | `60` |
| `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` |
| `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` |
| `RCLONE_CONFIG_DIR` | Path to the directory containing `rclone.conf` inside the container. Change this if running as a non-root user. | `/root/.config/rclone` |
diff --git a/apps/docs/content/docs/concepts/backups.mdx b/apps/docs/content/docs/concepts/backups.mdx
index 5ac023dd..f629eecc 100644
--- a/apps/docs/content/docs/concepts/backups.mdx
+++ b/apps/docs/content/docs/concepts/backups.mdx
@@ -114,6 +114,14 @@ Both methods produce identical snapshots. Manual runs are useful for verifying a
You can monitor backup progress in real time through the web interface. Zerobyte streams file counts, data processed, and upload progress as the backup runs.
+## Backup webhooks
+
+Backup jobs can run optional HTTP webhooks immediately before and after Restic. Pre-backup webhooks are useful for preparing the source, such as pausing a service or creating a dump. Post-backup webhooks are useful for cleanup, such as resuming a service after the snapshot.
+
+Pre-backup hook failures stop the backup before Restic runs. Post-backup hook failures are recorded with the run result; a clean backup becomes a warning if the post hook fails.
+
+For setup details and a container stop/start example, see [Backup Webhooks](/docs/guides/backup-webhooks).
+
## Backup status
The UI exposes two related status views:
diff --git a/apps/docs/content/docs/configuration.mdx b/apps/docs/content/docs/configuration.mdx
index 13204f34..eaacdb0b 100644
--- a/apps/docs/content/docs/configuration.mdx
+++ b/apps/docs/content/docs/configuration.mdx
@@ -38,6 +38,7 @@ Zerobyte is configured through environment variables and Docker Compose settings
| `TRUST_PROXY` | Set to `true` to trust `X-Forwarded-For` headers from a reverse proxy. | `false` |
| `TRUSTED_ORIGINS` | Comma-separated list of additional trusted origins for CORS. | (none) |
| `WEBHOOK_ALLOWED_ORIGINS` | Comma-separated list of HTTP origins allowed for backup webhooks and outbound HTTP notification destinations. | (none) |
+| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds. | `60` |
| `LOG_LEVEL` | Logging verbosity: `debug`, `info`, `warn`, `error`. | `info` |
| `SERVER_IDLE_TIMEOUT` | Server idle timeout in seconds. | `60` |
| `RCLONE_CONFIG_DIR` | Path to the rclone config directory inside the container. | `/root/.config/rclone` |
diff --git a/apps/docs/content/docs/guides/backup-webhooks.mdx b/apps/docs/content/docs/guides/backup-webhooks.mdx
new file mode 100644
index 00000000..535b4202
--- /dev/null
+++ b/apps/docs/content/docs/guides/backup-webhooks.mdx
@@ -0,0 +1,305 @@
+---
+title: Backup Webhooks
+description: Run HTTP hooks before and after a backup job
+---
+
+Backup webhooks let a backup job call an HTTP endpoint immediately before Restic starts and immediately after Restic finishes. Use them when the source needs a short runtime action around the backup, such as pausing a service, creating a database dump, flushing a cache, or resuming a container after the snapshot.
+
+Backup webhooks are configured per backup job in the **Advanced** section. They are different from [notifications](/docs/guides/notifications): notifications report backup events to people or systems, while backup webhooks are part of the backup execution lifecycle.
+
+## How backup webhooks work
+
+Zerobyte supports two lifecycle hooks:
+
+| Hook | When it runs | Failure behavior |
+| --- | --- | --- |
+| **Pre-backup webhook** | Before Restic starts reading the volume | A failed request stops the backup before Restic runs |
+| **Post-backup webhook** | After Restic finishes, fails, or is cancelled | A failed request is recorded with the final result; a clean backup becomes a warning |
+
+Each hook sends a `POST` request. A response with a `2xx` status code is treated as success. Redirects are not followed. Webhook requests time out after `WEBHOOK_TIMEOUT` seconds, which defaults to 60 seconds.
+
+
+ Every backup webhook URL must use an origin listed in `WEBHOOK_ALLOWED_ORIGINS`. The origin is the scheme, hostname, and port, such as `http://host.docker.internal:9000`.
+
+
+## Request body
+
+If the hook body field is empty, Zerobyte sends a JSON backup context body and sets `Content-Type: application/json`.
+
+Pre-backup webhook example:
+
+```json
+{
+ "phase": "pre",
+ "event": "backup.pre",
+ "jobId": "job_...",
+ "scheduleId": "sched_...",
+ "organizationId": "org_...",
+ "sourcePath": "/data"
+}
+```
+
+Post-backup webhook example:
+
+```json
+{
+ "phase": "post",
+ "event": "backup.post",
+ "jobId": "job_...",
+ "scheduleId": "sched_...",
+ "organizationId": "org_...",
+ "sourcePath": "/data",
+ "status": "success"
+}
+```
+
+`status` is only sent to the post-backup webhook. It can be `success`, `warning`, `error`, or `cancelled`. `error` is included on the post-backup webhook when Zerobyte has warning, failure, or cancellation details to report.
+
+If you enter a custom body, Zerobyte sends that exact body instead of the default JSON context. Add a `Content-Type` header yourself if the receiver expects one.
+
+## Headers
+
+Headers are optional and are entered one per line:
+
+```text
+X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret
+Content-Type: application/json
+```
+
+Header values are stored as plain text. Use a scoped webhook secret rather than a reusable account password or long-lived infrastructure token.
+
+## Configure a backup hook
+
+1. Add the webhook origin to `WEBHOOK_ALLOWED_ORIGINS` in the Zerobyte environment.
+2. Restart Zerobyte so the environment change is loaded.
+3. Open **Backups** and select the backup job.
+4. Edit the job and expand **Advanced**.
+5. Fill **Pre-backup webhook** or **Post-backup webhook**.
+6. Add any required headers.
+7. Leave the body empty unless the receiving service requires a custom payload.
+8. Save the backup job and run **Backup now** to test the lifecycle.
+
+For Docker Compose on Linux, `host.docker.internal` usually needs an explicit host gateway entry:
+
+```yaml docker-compose.yml
+services:
+ zerobyte:
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ environment:
+ - WEBHOOK_ALLOWED_ORIGINS=http://host.docker.internal:9000
+```
+
+## How-to: stop and start a Postgres container with adnanh/webhook
+
+This example runs [`adnanh/webhook`](https://github.com/adnanh/webhook) on the Docker host. Zerobyte calls it before and after the backup:
+
+- Pre-backup hook stops the `postgres` container.
+- Restic backs up the mounted data.
+- Post-backup hook starts the `postgres` container again.
+
+
+ Stopping a database container is a blunt consistency strategy. Use it only when a short outage is acceptable. For larger databases, prefer native database dumps, replication snapshots, or storage-level snapshots.
+
+
+### 1. Install webhook on the Docker host
+
+On Debian or Ubuntu:
+
+```bash
+sudo apt-get update
+sudo apt-get install webhook
+```
+
+`webhook` serves configured hooks at `/hooks/`. The default port is `9000`, and the `-hooks` flag points to the JSON or YAML hook file.
+
+### 2. Create hook scripts
+
+Create a directory for the scripts:
+
+```bash
+sudo mkdir -p /opt/zerobyte-hooks
+```
+
+Create `/opt/zerobyte-hooks/stop-postgres.sh`:
+
+```sh
+#!/bin/sh
+set -eu
+
+CONTAINER=postgres
+
+STATE=$(docker inspect -f '{{.State.Running}}' "$CONTAINER")
+
+if [ "$STATE" = "true" ]; then
+ docker stop "$CONTAINER"
+fi
+```
+
+Create `/opt/zerobyte-hooks/start-postgres.sh`:
+
+```sh
+#!/bin/sh
+set -eu
+
+CONTAINER=postgres
+
+STATE=$(docker inspect -f '{{.State.Running}}' "$CONTAINER")
+
+if [ "$STATE" != "true" ]; then
+ docker start "$CONTAINER"
+fi
+```
+
+Make both scripts executable:
+
+```bash
+sudo chmod +x /opt/zerobyte-hooks/stop-postgres.sh /opt/zerobyte-hooks/start-postgres.sh
+```
+
+If your container has a different name, change `CONTAINER=postgres` in both scripts.
+
+### 3. Create the webhook config
+
+Create `/opt/zerobyte-hooks/hooks.json`:
+
+```json
+[
+ {
+ "id": "stop-postgres",
+ "execute-command": "/opt/zerobyte-hooks/stop-postgres.sh",
+ "command-working-directory": "/opt/zerobyte-hooks",
+ "http-methods": ["POST"],
+ "include-command-output-in-response": true,
+ "trigger-rule": {
+ "match": {
+ "type": "value",
+ "value": "replace-with-a-long-random-secret",
+ "parameter": {
+ "source": "header",
+ "name": "X-Zerobyte-Hook-Secret"
+ }
+ }
+ }
+ },
+ {
+ "id": "start-postgres",
+ "execute-command": "/opt/zerobyte-hooks/start-postgres.sh",
+ "command-working-directory": "/opt/zerobyte-hooks",
+ "http-methods": ["POST"],
+ "include-command-output-in-response": true,
+ "trigger-rule": {
+ "match": {
+ "type": "value",
+ "value": "replace-with-a-long-random-secret",
+ "parameter": {
+ "source": "header",
+ "name": "X-Zerobyte-Hook-Secret"
+ }
+ }
+ }
+ }
+]
+```
+
+Use the same secret in both hook definitions. `include-command-output-in-response` makes `webhook` wait for the script and return an error response if the command fails, which lets Zerobyte stop the backup when the pre-backup hook cannot stop Postgres.
+
+### 4. Start webhook
+
+Run it in the foreground first:
+
+```bash
+sudo webhook -hooks /opt/zerobyte-hooks/hooks.json -port 9000 -verbose -http-methods POST
+```
+
+In another shell, test both hooks:
+
+```bash
+curl -X POST \
+ -H "X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret" \
+ http://localhost:9000/hooks/stop-postgres
+
+curl -X POST \
+ -H "X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret" \
+ http://localhost:9000/hooks/start-postgres
+```
+
+Once the test works, run `webhook` under your normal process manager.
+
+### 5. Allow Zerobyte to call the webhook server
+
+Add the webhook server origin to Zerobyte:
+
+```yaml docker-compose.yml
+services:
+ zerobyte:
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ environment:
+ - WEBHOOK_ALLOWED_ORIGINS=http://host.docker.internal:9000
+```
+
+Restart Zerobyte:
+
+```bash
+docker compose up -d
+```
+
+### 6. Add the hooks to the backup job
+
+Open the backup job in Zerobyte, edit it, and expand **Advanced**.
+
+Use these values:
+
+```text
+Pre-backup webhook: http://host.docker.internal:9000/hooks/stop-postgres
+Pre-backup webhook headers:
+X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret
+
+Post-backup webhook: http://host.docker.internal:9000/hooks/start-postgres
+Post-backup webhook headers:
+X-Zerobyte-Hook-Secret: replace-with-a-long-random-secret
+```
+
+Leave both body fields empty. Zerobyte will send the default JSON context body.
+
+Run **Backup now**. If the stop hook fails or returns a non-`2xx` response, Zerobyte fails the backup before Restic starts. If the start hook fails after Restic finishes, Zerobyte records the problem in the run details so you can restart the container manually.
+
+### 7. Run webhook as a service
+
+After the foreground test works, create a small systemd unit so `webhook` starts on boot.
+
+Create `/etc/systemd/system/zerobyte-webhook.service`:
+
+```ini
+[Unit]
+Description=Zerobyte backup webhook runner
+After=network-online.target docker.service
+Wants=network-online.target
+Requires=docker.service
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/webhook -hooks /opt/zerobyte-hooks/hooks.json -port 9000 -http-methods POST -verbose
+Restart=on-failure
+RestartSec=5s
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Enable and start it:
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable --now zerobyte-webhook.service
+sudo systemctl status zerobyte-webhook.service
+```
+
+Check logs with:
+
+```bash
+sudo journalctl -u zerobyte-webhook.service -f
+```
+
+import { Callout } from "fumadocs-ui/components/callout";
diff --git a/apps/docs/content/docs/guides/meta.json b/apps/docs/content/docs/guides/meta.json
index 1f6a6767..7709de28 100644
--- a/apps/docs/content/docs/guides/meta.json
+++ b/apps/docs/content/docs/guides/meta.json
@@ -3,6 +3,7 @@
"pages": [
"3-2-1-backup-strategy",
"restoring",
+ "backup-webhooks",
"notifications",
"recovery-key-and-repository-passwords",
"repository-maintenance",
diff --git a/apps/docs/content/docs/installation.mdx b/apps/docs/content/docs/installation.mdx
index 2621de98..0068f235 100644
--- a/apps/docs/content/docs/installation.mdx
+++ b/apps/docs/content/docs/installation.mdx
@@ -115,6 +115,7 @@ The `BASE_URL` determines cookie security behavior:
| `PORT` | Port the web interface listens on inside the container | `4096` |
| `RESTIC_HOSTNAME` | Hostname used by Restic in snapshots | `zerobyte` |
| `TRUSTED_ORIGINS` | Comma-separated list of additional trusted CORS origins | (none) |
+| `WEBHOOK_TIMEOUT` | Timeout for backup webhook requests in seconds | `60` |
| `LOG_LEVEL` | Logging verbosity: `debug`, `info`, `warn`, `error` | `info` |
| `SERVER_IDLE_TIMEOUT` | Server idle timeout in seconds | `60` |
| `RCLONE_CONFIG_DIR` | Path to rclone config directory inside container | `/root/.config/rclone` |