Webhooks
Webhooks
An organisation can register endpoints that receive a POST when something
happens. Managing them needs webhook.manage.
Events
Subscribe to any subset. An empty selection means every event.
| Event | Fires when |
|---|---|
blueprint.published | A blueprint goes draft → published, or a draft sibling is merged into its published parent |
blueprint.unpublished | A blueprint is deprecated, set back to draft, or hard-deleted |
blueprint.visibility.changed | Visibility changes via the Share dialog or the wizard |
folium.published | A folium version is published: a frozen snapshot of the whole tree |
folium.page.updated | A page's content or metadata is saved, in the live working set |
folium.page.created | A page is added to a folium |
workflow.run.submitted | An author hands a draft into an approval workflow |
workflow.run.approved | The last step of a run is approved |
workflow.run.rejected | An approver rejects at any step |
member.added | Someone joins the org, by invite or direct add |
member.removed | Someone is removed, or leaves |
The request
POST https://your-endpoint.example.com/blueprintr
Content-Type: application/json
User-Agent: Blueprintr-Webhooks/1
X-Blueprintr-Event: folium.page.updated
X-Blueprintr-Delivery: cmtp0q7uv0002mohxnqod3vkb
X-Blueprintr-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015…
{
"id": "cmtp0q7uv0002mohxnqod3vkb",
"event": "folium.page.updated",
"orgId": "cmrdzv7ts0001fnywvijnxgaz",
"occurredAt": "2026-09-05T23:04:11.882Z",
"payload": { }
}
id is the delivery id and doubles as your dedupe key. It is also in the
X-Blueprintr-Delivery header, so you can dedupe before parsing the body.
Verifying
X-Blueprintr-Signature is sha256= followed by the hex HMAC-SHA256 of the
exact bytes of the request body, keyed with the endpoint's secret. Read
the raw body. A framework that parses and re-serialises JSON for you will
change key order or whitespace, and every signature will fail.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
Compare in constant time. A plain === leaks the correct signature one byte at
a time to anyone who can measure your response latency.
An unverified endpoint is an unauthenticated write path into whatever it triggers. Verify on every request and reject what fails. Do not log and continue.
Provider shapes
An endpoint has a provider mode, and it changes the body on the wire:
| Provider | Body |
|---|---|
generic | The envelope above |
slack | A Slack incoming-webhook payload: { text, blocks } |
discord | A Discord webhook payload: { content, embeds } |
msteams | A Teams MessageCard |
The signature is computed over what is sent, so a Slack-shaped delivery is
signed over the Slack JSON, not over the envelope. Point a slack
endpoint at a Slack incoming-webhook URL and it works with no receiver of your
own.
Endpoint requirements
The URL must be HTTPS on a public host. Private, link-local and metadata addresses are refused, and the connection is pinned to the address that was validated, so a hostname that resolves to something public during the check and to something internal a moment later still cannot be reached.
| Request timeout | 5 seconds |
| Response body read | first 1 KB, then discarded |
Return a 2xx quickly and do the work afterwards. A slow endpoint is a failed delivery.
There are no retries
A failed delivery is recorded and not retried. There is no backoff and no redelivery queue, so if your endpoint was down, that event is gone.
Treat webhooks as a low-latency hint that something changed, and reconcile
against the REST API for
anything you cannot afford to miss. Ordering is not guaranteed either, so make
handlers idempotent on id.
Every attempt, succeeded or failed, is recorded with its response status and error.
Testing
Send test on the webhook posts a real signed delivery to the endpoint and records the result in the same log. Use it to prove the signature check before subscribing.
Rotating a secret
Rotation is a hard cutover. The new secret signs the next delivery, and every receiver still verifying with the old one starts failing immediately.
Deploy it accepting either the current secret or an as-yet-unset second one, before you rotate.
From the webhook's settings. You will be asked to reauthenticate: rotation hands out a live signing secret and breaks every receiver, so a stolen session cannot do it alone. Limited to five rotations an hour.
Once a delivery has verified against the new one.