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.

View as Markdown

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

EventSent whendata
content.recalledSomeone 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.restoredRecalled 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

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.

CallWhat it does
GET /v1/webhooksList 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}/testSend a real signed delivery now.
GET /v1/webhooks/deliveriesThe delivery log, with payload, response and attempts. Paginated. Kept 30 days.
POST /v1/webhooks/deliveries/{id}/replaySend 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
{
  "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.

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'));
}
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.