Skip to main content

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.

EventFires when
blueprint.publishedA blueprint goes draft → published, or a draft sibling is merged into its published parent
blueprint.unpublishedA blueprint is deprecated, set back to draft, or hard-deleted
blueprint.visibility.changedVisibility changes via the Share dialog or the wizard
folium.publishedA folium version is published: a frozen snapshot of the whole tree
folium.page.updatedA page's content or metadata is saved, in the live working set
folium.page.createdA page is added to a folium
workflow.run.submittedAn author hands a draft into an approval workflow
workflow.run.approvedThe last step of a run is approved
workflow.run.rejectedAn approver rejects at any step
member.addedSomeone joins the org, by invite or direct add
member.removedSomeone 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:

ProviderBody
genericThe envelope above
slackA Slack incoming-webhook payload: { text, blocks }
discordA Discord webhook payload: { content, embeds }
msteamsA 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 timeout5 seconds
Response body readfirst 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.

1
Teach the receiver both secrets

Deploy it accepting either the current secret or an as-yet-unset second one, before you rotate.

2
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.

3
Drop the old secret

Once a delivery has verified against the new one.