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

# Operations

> Catalogue sync, test deliveries, the development sink, dashboard and health signals, permissions and known limits of the first release.

## Catalogue sync

The `event` table must mirror the generated catalogue before subscribers can
pick events. Two mechanisms keep it that way:

| When                              | What runs                                                                                                                                                                          |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Every pull request                | `webhook:catalogue:generate --check` and `webhook:catalogue:docs --check` in the CI **Quality** gate — the code catalogue and this documentation cannot drift from the route tree. |
| Every deploy that runs migrations | The `catalogue-sync` job of `database-migrate.yml` executes `webhook:catalogue:sync --env <tier>` right after Drizzle migrations.                                                  |
| On demand                         | `pnpm --filter @bbrandslab/horizon-api webhook:catalogue:sync --env development` (or `certification`, `production`).                                                               |

Verify with:

```bash theme={null}
curl https://api.bbrands.io/api/v3/webhook/catalogue/status \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "data": {
    "generated_count": 1438,
    "in_sync": true,
    "missing_internals": [],
    "stale_internals": [],
    "synced_count": 1438
  }
}
```

`missing_internals` lists events present in code but not yet in the
database; `stale_internals` lists database rows no route produces anymore
(they stay subscribable until an operator deactivates them).

## Test delivery

`POST /api/v3/webhook/webhook/{id}/test` enqueues a synthetic
`webhook.test` delivery to that subscriber and returns the `dispatch_id`.
Follow it on `GET /api/v3/webhook/dispatch/{dispatch_id}` or on the detail
screen in Horizon Enterprise. The webhook does **not** need to be subscribed
to `webhook.test` for the action to work; subscribing simply lets you filter
those deliveries later.

## Development sink

`POST /api/v3/webhook/sink` is a built-in subscriber endpoint mounted only
when `APP_ENV` is `development` or `certification` **and**
`WEBHOOK_SINK_SECRET` is set. It verifies the signature with that secret,
logs `Webhook sink delivery accepted` with the envelope headers, and answers
`200`, so a dispatch row flips to `success` without any external system.

<Steps>
  <Step title="Create a webhook pointing at the sink">
    `url = https://<api host>/api/v3/webhook/sink`, `is_global = true`.
    Copy the returned `whsec_…` secret.
  </Step>

  <Step title="Share the secret with the API">
    Set `WEBHOOK_SINK_SECRET` to that value in the tier's environment and
    redeploy (or restart locally).
  </Step>

  <Step title="Send a test">
    `POST /api/v3/webhook/webhook/{id}/test`, then check the dispatch row and
    the API logs.
  </Step>
</Steps>

Production never mounts the sink: the route answers `404` there regardless
of the variable.

## Dashboard and health

| Signal            | Where                                                         | Content                                                                                                                                                                         |
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dashboard summary | `GET /api/v3/webhook/dashboard/summary` (`webhook:dashboard`) | Deliveries by status in the last 24 h, active vs auto-disabled webhooks, catalogue counts and the 10 most recent failures. Rendered on the Webhooks home in Horizon Enterprise. |
| Health component  | `GET /api/v3/health/component/integration/webhook-out`        | `operational`, or `degraded` when the last 24 h show ≥ 10 exhausted deliveries or any auto-disabled webhook. Ledger-driven; it never reports `down`.                            |
| Metrics           | `webhook_out_deliveries_total{status}`                        | All-time delivery counts per status, pushed by the metrics job to the status page.                                                                                              |

## Environment variables

| Variable                 | Purpose                                                                |
| ------------------------ | ---------------------------------------------------------------------- |
| `ENABLE_API_EVENT_QUEUE` | Master switch for publishing. `false` = no events leave the API.       |
| `WEBHOOK_SINK_SECRET`    | Secret of the webhook that targets the development/certification sink. |

The signature tolerance (300 s), the backoff schedule, the attempt cap (5)
and the auto-disable threshold (20) are code constants in
`lib/webhook/outbound/webhook-out.constants.ts` and
`webhook-signature.ts`, not environment variables — they are part of the
public contract and change only with a release.

## Permissions

All webhook permissions are granted to the `administrator` role only in this
release. Tokens for automation must carry the specific internals:

| Action                           | Permission                                                                          |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| List / read webhooks             | `webhook:find-all`, `webhook:find-one`                                              |
| Create / update / delete / purge | `webhook:create`, `webhook:update`, `webhook:delete`, `webhook:purge`               |
| Activate / deactivate            | `webhook:active`                                                                    |
| Rotate secret, test delivery     | `webhook:rotate-secret`, `webhook:test`                                             |
| Export                           | `webhook:export`                                                                    |
| Dashboard, catalogue status      | `webhook:dashboard`, `webhook:catalogue-status`                                     |
| Replace the event set            | `webhook-event:set` (plus the CRUD set `webhook-event:*`)                           |
| Read the ledger, replay          | `webhook-dispatch:find-all`, `webhook-dispatch:find-one`, `webhook-dispatch:replay` |
| Browse the catalogue             | `event:find-all`, `event:find-one`                                                  |

## Known limits

* **No fixed egress IP.** Deliveries originate from the API's serverless
  runtime; allow-list by signature, not by address.
* **Thin body.** The payload never includes the resource; a second API call is
  required to read it.
* **At-least-once.** Retries and replays may repeat a delivery; deduplicate on
  `id`.
* **No ordering guarantee** across events, even for the same resource.
* **Secret rotation has no overlap window.**
* **Purge events** carry `data.account = null` and only reach global
  subscribers.

## Related

* Internal runbook for the first rollout (per-tier steps, rollback, day-2
  signals): `docs/initiatives/webhook/outbound/11-rollout-and-operations.md`
  in the monorepo.
* API reference: [Webhook](/api-reference/webhook/webhook/find-all),
  [Webhook Event](/api-reference/webhook/webhook-event/find-all),
  [Webhook Dispatch](/api-reference/webhook/webhook-dispatch/find-all),
  [Event](/api-reference/webhook/event/find-all).
