# Webhooks

> Get a signed HTTPS POST when Brix content is recalled or restored. Create endpoints, verify the brix-signature HMAC, handle retries, and replay deliveries.

Source: https://brixsignage.com/developers/webhooks/

A webhook endpoint receives a signed `POST` from Brix when an event happens in your workspace. Manage endpoints in the CMS at **Settings > Integrations > Webhooks**, or over the API. Managing webhooks needs `integration.view` and `integration.edit` at the workspace level.

## Events

| Event | Sent when | `data` |
| --- | --- | --- |
| `content.recalled` | Someone pulls a file, design, playlist or schedule off every screen. | `kind` (`media`, `creative`, `playlist`, `schedule`), `id`, `name` (the kind's label, such as `Playlist`), `reason` (or null), `recalledBy` |
| `content.restored` | Recalled content is put back. | `kind`, `id`, `name` |

`GET /v1/webhooks/events` returns the full event catalog with a label and description for each. The catalog also names screen, approval and emergency events (`screen.offline`, `screen.online`, `approval.requested`, `approval.decided`, `emergency.started`, `emergency.cleared`). You can subscribe to them, but Brix does not send them. Only the two events above are delivered.

## Create an endpoint

```bash
curl -X POST https://api.brixsignage.com/v1/webhooks \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/brix-hook", "events": ["content.recalled", "content.restored"], "description": "Ops channel"}'
```

The response is `201` with the endpoint (`id` starts with `whe_`) and its signing `secret`. The secret is shown once. Store it. An unknown event name returns `400`.

| Call | What it does |
| --- | --- |
| `GET /v1/webhooks` | List endpoints. The secret is never included. |
| `PATCH /v1/webhooks/{id}` | Change `url`, `events`, `description` or `enabled`. Re-enabling clears an auto-disable. |
| `DELETE /v1/webhooks/{id}` | Delete an endpoint. Its pending deliveries stop. |
| `POST /v1/webhooks/{id}/test` | Send a real signed delivery now. |
| `GET /v1/webhooks/deliveries` | The delivery log, with payload, response and attempts. Paginated. Kept 30 days. |
| `POST /v1/webhooks/deliveries/{id}/replay` | Send a delivery again as a new delivery. |

The test delivery is a `screen.offline` event with `data.test: true` and a note that nothing is wrong. It returns `{ deliveryId, ok, status, error }`. If it returns `409 not_subscribed`, add `screen.offline` to the endpoint's events.

## The request Brix sends

```
POST /brix-hook HTTP/1.1
content-type: application/json
user-agent: Brix-Webhooks/1
x-brix-event: content.recalled
brix-signature: t=1790000000,v1=5f2b...c9
```

```json
{
  "deliveryId": "whd_...",
  "event": "content.recalled",
  "occurredAt": "2026-01-15T14:03:22.000Z",
  "workspaceId": "...",
  "attempt": 1,
  "data": { "kind": "playlist", "id": "pl_...", "name": "Playlist", "reason": "Wrong prices", "recalledBy": "..." }
}
```

- `deliveryId` stays the same across retries. Use it to ignore duplicates.
- `occurredAt` is when the event happened, not when it was sent.
- `attempt` counts delivery attempts, starting at 1.
- A replay has a new `deliveryId` and a `replayOf` field with the original id.

## Verify the signature

`brix-signature` is `t=<unix seconds>,v1=<hex>`. The hex is HMAC-SHA256 of `<t>.<raw body>` with your endpoint secret. Check it against the raw body bytes, before you parse the JSON, and reject old timestamps.

```js
import crypto from 'node:crypto';

export function verifyBrix(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries((header ?? '').split(',').map((p) => p.trim().split('=', 2)));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}
```

```python
import hmac, hashlib, time

def verify_brix(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## Delivery and retries

- Delivery is at least once. Deduplicate on `deliveryId`.
- Any `2xx` response is success. Redirects are not followed. Brix waits 10 seconds for a response.
- A failed delivery is retried with backoff: 30 seconds, 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, then 4 hours between attempts. After the ninth attempt the delivery is abandoned.
- After 9 failures in a row, Brix disables the endpoint and emails the workspace Owners. Fix the receiver, then set `enabled: true` again.
- Reply fast and do the work after. A slow receiver risks the 10-second timeout and a duplicate.
