> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dbhost.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks API: operation events and signatures

> Register HTTPS endpoints for DBHost operation events, rotate signing secrets and verify the HMAC-SHA256 signature on each delivery.

DBHost can send an HTTPS `POST` to your endpoint when an asynchronous
operation, such as a backup or restore, reaches a final status.

* These routes require a **full-account** API key. A selected-database key
  receives `403 KEY_SCOPE_DENIED`. See [API keys](/dashboard/api-keys).
* API access requires a Pro or Business plan.

## Events

Each event name is `<operation type>.<final status>`:

| Operation type | Events |
| - | - |
| `backup` | `backup.succeeded`, `backup.failed`, `backup.canceled` |
| `restore` | `restore.succeeded`, `restore.failed`, `restore.canceled` |
| `clone` | `clone.succeeded`, `clone.failed`, `clone.canceled` |
| `delete` | `delete.succeeded`, `delete.failed`, `delete.canceled` |
| `recover` | `recover.succeeded`, `recover.failed`, `recover.canceled` |
| `extension` | `extension.succeeded`, `extension.failed`, `extension.canceled` |

Handle unknown event names gracefully; new operation types can add events.
Events are sent only for final statuses, not for `queued` or `running`. An
endpoint registered without `databaseId` receives events for every database in
the account. An endpoint registered with `databaseId` receives events for that
database only.

### Payload

```json theme={null}
{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "event": "backup.succeeded",
  "createdAt": "2026-10-14T09:02:00.000Z",
  "data": {
    "operationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "databaseId": "550e8400-e29b-41d4-a716-446655440000",
    "databaseName": "my-app",
    "status": "succeeded",
    "stage": "completed",
    "progress": 100,
    "error": null
  }
}
```

`id` and `data.operationId` are the operation ID. Read the full operation,
including its `result`, with [Get operation](/api-reference/operations/get).
For a failed operation, `error` is `{"code": "...", "message": "..."}`.

### Request headers

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `User-Agent` | `DBHost-Webhooks/1.0` |
| `DBHost-Event-Id` | Unique per event and endpoint. Retries of the same delivery reuse it; use it to ignore duplicates. |
| `DBHost-Timestamp` | Unix time in seconds when this attempt was signed |
| `DBHost-Signature` | `v1=` followed by a lowercase hex HMAC-SHA256 digest |

## Verify the signature

DBHost signs each attempt with your endpoint's signing secret:

1. Build the signed string: the `DBHost-Timestamp` value, a `.`, then the raw
   request body exactly as received.
2. Compute HMAC-SHA256 over that string. Use the full secret, including the
   `whsec_` prefix, as the key.
3. Hex-encode the digest, prefix it with `v1=`, and compare it to
   `DBHost-Signature` in constant time.

Verify against the raw body before parsing JSON. DBHost does not enforce a
timestamp window; reject timestamps that are too old for your needs (for
example more than five minutes) to limit replay.

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyDbhostWebhook(rawBody, headers, secret) {
  const timestamp = headers["dbhost-timestamp"];
  const signature = headers["dbhost-signature"] ?? "";
  const expected =
    "v1=" +
    createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  return a.length === b.length && timingSafeEqual(a, b) && ageSeconds <= 300;
}
```

## Delivery and retries

* A `2xx` response within 10 seconds counts as delivered. Redirects are not
  followed and count as a failure.
* A failed delivery is retried up to five attempts in total, with growing
  delays (about 30 seconds, then 1, 2 and 4 minutes, plus jitter).
* A successful delivery resets the endpoint's `consecutiveFailures` to `0`.
* After a delivery has used all attempts, DBHost disables the endpoint if it
  has 10 or more consecutive failed attempts. Its `enabled` becomes `false`
  and `disabledAt` is set. Rotating the secret enables it again.

## Endpoint URL rules

* The URL must be an absolute `https://` URL without a username or password.
* `localhost`, `*.localhost`, `*.local` and `*.internal` hostnames are
  rejected.
* The hostname must resolve, and every address must be public. Private,
  loopback, link-local, carrier-grade NAT, multicast and documentation ranges
  are rejected. DBHost checks this again on every delivery.
* Any `#fragment` is removed.

## List endpoints

`GET /api/v1/webhooks`

```http theme={null}
GET /api/v1/webhooks
Authorization: Bearer <api-key>
```

```json 200 OK theme={null}
{
  "data": [
    {
      "id": "d4e5f6a7-0000-4000-8000-000000000003",
      "databaseId": null,
      "url": "https://hooks.example.com/dbhost",
      "enabled": true,
      "consecutiveFailures": 0,
      "disabledAt": null,
      "createdAt": "2026-10-14T09:00:00.000Z",
      "updatedAt": "2026-10-14T09:00:00.000Z"
    }
  ]
}
```

The list includes disabled endpoints. The signing secret is never returned
here.

## Create an endpoint

`POST /api/v1/webhooks`

| Field | Type | Required | Description |
| - | - | - | - |
| `url` | string | Yes | HTTPS URL that follows the [URL rules](#endpoint-url-rules) |
| `databaseId` | string | No | Limit events to one database you own. Omit for all databases. |

```http theme={null}
POST /api/v1/webhooks
Authorization: Bearer <api-key>
Content-Type: application/json

{"url":"https://hooks.example.com/dbhost"}
```

```json 201 Created theme={null}
{
  "data": {
    "id": "d4e5f6a7-0000-4000-8000-000000000003",
    "databaseId": null,
    "url": "https://hooks.example.com/dbhost",
    "enabled": true,
    "createdAt": "2026-10-14T09:00:00.000Z",
    "secret": "whsec_<signing-secret>"
  }
}
```

<Warning>
  `secret` is shown only in this response and in the rotate response. Store it
  securely; DBHost cannot show it again.
</Warning>

## Rotate the signing secret

`POST /api/v1/webhooks/{id}/rotate`

Creates a new signing secret. The old secret stops being used immediately,
including for retries of earlier events. Rotating also re-enables a disabled
endpoint and resets `consecutiveFailures` to `0`. No request body is needed.

```http theme={null}
POST /api/v1/webhooks/{id}/rotate
Authorization: Bearer <api-key>
```

```json 200 OK theme={null}
{
  "message": "Webhook signing secret rotated.",
  "data": {
    "id": "d4e5f6a7-0000-4000-8000-000000000003",
    "databaseId": null,
    "url": "https://hooks.example.com/dbhost",
    "enabled": true,
    "updatedAt": "2026-10-14T10:00:00.000Z",
    "secret": "whsec_<new-signing-secret>"
  }
}
```

## Disable an endpoint

`DELETE /api/v1/webhooks/{id}`

Disables the endpoint: it stops receiving events, `enabled` becomes `false`
and `disabledAt` is set. The endpoint stays in the list. Returns
`204 No Content` with an empty body. To enable it again, rotate its secret.

```http theme={null}
DELETE /api/v1/webhooks/{id}
Authorization: Bearer <api-key>
```

## Errors

| Status | Code | Description |
| - | - | - |
| 400 | `VALIDATION_ERROR` | `url` is missing |
| 400 | `INVALID_WEBHOOK_ENDPOINT` | The URL breaks a [URL rule](#endpoint-url-rules), or `databaseId` is not a database you own |
| 400 | `INVALID_JSON` | Request body is not valid JSON |
| 401 | `UNAUTHORIZED` | Missing or invalid API key |
| 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business |
| 403 | `KEY_SCOPE_DENIED` | The key does not have full account access |
| 404 | `WEBHOOK_NOT_FOUND` | No endpoint with this ID in your account |
| 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB |
| 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds |
| 500 | `WEBHOOK_ROTATION_FAILED` | The secret could not be rotated; retry |
| 500 | `WEBHOOK_UPDATE_FAILED` | The endpoint could not be disabled; retry |

The CLI equivalents are `dbhost webhooks list`,
`dbhost webhooks create <https-url> [database-id]` and
`dbhost webhooks rotate|disable <webhook-id>`. API keys are managed in
[API keys](/dashboard/api-keys); see also
[Authentication](/api-reference/authentication).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.