Webhooks

​

Register HTTPS endpoints and Brellium sends each one a signed JSON POST when a subscribed event happens (today: audit.completed). For each endpoint you choose the payload fields, filter by question set or by flagged result, and can add receiver authentication. Every delivery and attempt is recorded, and every configuration change is written to the activity log.

Access

Reads need the webhooks:read scope. Changes, test sends, resends and recovery need webhooks:write. Existing API keys do not gain these scopes: a key's scopes are fixed when the key is created, and rotating its secret does not change them. To use these routes, create a new key with webhooks:read and webhooks:write in the dashboard (API keys → Create key), move your integration to it, then revoke the old key. A token without the scope gets 403 with Missing required scope: webhooks:read (or webhooks:write).

Verifying signatures

Deliveries follow the Standard Webhooks specification. Each request carries:

  • webhook-id: the event id, equal to the body id. It stays the same across retries and resends, so use it to drop duplicates.
  • webhook-timestamp: the attempt time, in Unix seconds.
  • webhook-signature: one or more space-separated v1,<base64> values. Each is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed with the endpoint's whsec_… secret. During a secret rotation there is one signature per active secret; accept the request if any of them matches.

Verify the raw request body (before JSON parsing) and reject old timestamps. The official standardwebhooks libraries do both, with a 5-minute tolerance.

Node (npm install standardwebhooks):

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.BRELLIUM_WEBHOOK_SECRET); // "whsec_..."

// rawBody: the exact request body as a string.
// Throws WebhookVerificationError when the signature or timestamp is bad.
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
});

Python (pip install standardwebhooks):

import os
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["BRELLIUM_WEBHOOK_SECRET"])  # "whsec_..."

# raw_body: the exact request body (str or bytes).
# Raises WebhookVerificationError when the signature or timestamp is bad.
event = wh.verify(raw_body, dict(request.headers))

Legacy signature. When signing.legacy_signature is true, deliveries also carry X-Brellium-Signature: sha256=<hex>: the hex HMAC-SHA256 of the raw body, keyed with the whole whsec_… string as UTF-8. It exists for receivers built for Brellium's earlier per-org webhooks. New integrations should verify webhook-signature.

Delivery and retries

A delivery succeeds on any 2xx response within 15 seconds. Anything else is a failure, including redirects (which are never followed). Failed deliveries are retried after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h, 24 h (±20% jitter), for at most 10 attempts over about 3 days. A 410 Gone response disables the endpoint. An endpoint whose attempts all fail for 5 days is disabled automatically. While an endpoint is disabled (by you, by a 410, or automatically), events that occur are not queued for it and cannot be recovered later. Deliveries that were already queued are marked failed; after you re-enable the endpoint, recover can queue them again. Test events are attempted once and are limited to 10 per minute per organization.

Brellium API