Brault Developers

Webhooks

The full event catalogue, delivery headers, and how to verify a payload's signature.

See Integrate webhooks for how to register an endpoint and what delivery behavior (retries, auto-disable) to expect. This page covers what you receive and how to trust it.

Headers on every delivery

HeaderContents
Brault-Signaturet=<unix timestamp>,v1=<hex HMAC> — see verification below.
Brault-Webhook-IdThe endpoint that received this delivery.
Brault-Event-IdMatches id in the payload — the deduplication key.
Brault-Event-TypeOne catalogue token, e.g. file.created.
Brault-Delivery-IdThis specific delivery attempt's record id.
Brault-Delivery-Attempt16.
X-Request-IdCorrelates this delivery with the platform request log.
User-AgentBrault-Webhooks/1.0
Content-Typeapplication/json; charset=utf-8
Content-LengthByte length of the exact signed body — never chunked transfer.

Verifying the signature

The signed material is <t>.<raw request body> — the exact bytes you received, before any re-serialization. Read the raw body first; don't verify against JSON.stringify of a parsed object, since key order or whitespace differences would silently break every signature.

Brault-Signature: t=1725609600,v1=5257a869e7bfbe86...
  1. Split the header on ,, then split each part on the first =.
  2. Reject the delivery if |now − t| > 300 seconds (5 minutes) — this bounds replay of an intercepted request.
  3. Compute hex(hmac-sha256(secret, "<t>.<raw body>")).
  4. Accept if it matches any v1 entry present. There is normally exactly one — but for 24 hours after you roll a webhook's secret, deliveries carry two v1 entries (current secret first, then the previous one), so that you can pick up the new secret at your own pace without dropping events mid-rotation.

This bash version is a debugging aid for a quick terminal check, not a template for production code: it doesn't compare in constant time, and the secret is briefly visible on the command line (ps) while it runs. For production verification, use the JavaScript or Python implementation below.

#!/usr/bin/env bash
# Usage: verify.sh <raw-body-file> "<Brault-Signature header value>" <webhook-secret>
body_file="$1"
signature_header="$2"
secret="$3"

t=$(echo "$signature_header" | grep -oE 't=[0-9]+' | head -1 | cut -d= -f2)
now=$(date +%s)
age=$(( now - t ))
[ "${age#-}" -gt 300 ] && { echo "signature too old" >&2; exit 1; }

expected=$(openssl dgst -sha256 -mac HMAC -macopt "key:$secret" -binary \
    < <(printf '%s.' "$t"; cat "$body_file") \
  | od -An -tx1 | tr -d ' \n')

echo "$signature_header" | grep -oE 'v1=[0-9a-f]+' | cut -d= -f2 | grep -qx "$expected" \
  && echo "valid" \
  || { echo "invalid" >&2; exit 1; }

The event envelope

Every delivery's body looks like this:

{
  "id": "evt_c0ffee…",
  "object": "event",
  "type": "file.created",
  "occurred_at": "2026-09-05T10:15:00.000Z",
  "brandspace_id": "clx…",
  "actor": { "type": "user", "id": "clu…" },
  "data": { "object": "file", "id": "clf…", "name": "hero.psd", "library_id": "clw…", "folder_id": null },
  "attempt": 1
}

data is a minimal snapshot (ids and names, not the full resource) — re-fetch the resource from the API if you need more. actor.type is one of four values: user (the common case, including events caused by your own key's requests — the key acts as its creator), api_key (only on webhook.test and the two import events), system (an automated cause with no person behind it — crons, cascades, the media-processor callback — always id: null), or anonymous (an unauthenticated visitor: a shared-link visitor or a transfer recipient, always id: null) — see Integrate webhooks for what this means for de-duplicating your own writes.

Event catalogue

events: ["*"] subscribes to every event below, including any a later version adds — the catalogue only ever grows additively. Fetch it live (any scope will do — the route still needs a key) at GET /v1/events, or see the Meta reference for the exact shape of each row.

Files (10)

file.created, file.updated, file.processed, file.moved, file.trashed, file.restored, file.deleted, file.version.created, file.version.activated, file.version.deleted

Folders and libraries (9)

folder.created, folder.updated, folder.moved, folder.trashed, folder.restored, folder.deleted, library.created, library.updated, library.deleted

Boards and properties (13)

board.created, board.updated, board.deleted, board.file.added, board.file.removed, board.property.created, board.property.updated, board.property.deleted, board.file.property_updated, property.created, property.updated, property.deleted, property.value_updated

Comments (8)

comment.created, comment.updated, comment.resolved, comment.reopened, comment.deleted, reply.created, reply.updated, reply.deleted

Pages (5)

page.created, page.updated, page.published, page.unpublished, page.deleted

page.updated means the page's content was saved — a page rename, tag change, move or restore arrives as the corresponding file.* event instead, with data.file_type: "canvas" telling you it was a page.

Sharing and members (6)

shared_link.created, shared_link.updated, shared_link.deleted, member.added, member.removed, member.role_changed

Imports (2)

import.completed, import.failed — report the terminal state of a POST /v1/files/import job. A successful import also produces a normal file.created; the two are not the same signal and both fire.

Transfers (4)

transfer.created, transfer.ready, transfer.downloaded, transfer.expired — the life of a transfer link. transfer.ready is the one that says the zip finished and zip_url now resolves; transfer.expired fires whenever the transfer stops being available, whether it expired on its own, was ended early, or its zip could not be built. A transfer you ended early with DELETE gets transfer.expired twice: once at the call and once more when the nightly cleanup removes the row, with a different event id each time, so dedupe on data.id. transfer.downloaded carries actor.type: "anonymous" for a recipient with no account, which is the usual case.

Platform (1)

webhook.test — the synthetic event POST /v1/webhooks/:id/test sends.

On this page