Console
Webhooks
Receive signed events from Brunel. Event catalog with example bodies, the X-Brunel-Signature header and how to verify it, delivery log, redelivery and retention.
Webhooks are available on Business and above. Set the endpoint in Platform → Notifications, then follow deliveries in Platform → Webhooks.
Delivery
Brunel sends a POST with a JSON body to your HTTPS endpoint:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Brunel-Webhooks |
X-Brunel-Event | the event, e.g. tracking.alert |
X-Brunel-Delivery | the delivery id |
X-Brunel-Signature | sha256=<HMAC-SHA256 of the raw body with your signing secret> |
Answer with any 2xx within 5 seconds. Redirects are not followed. Endpoints must be public addresses: private and internal ranges are refused, checked at each delivery.
Body
{
"id": "evt_Q2m9x3…",
"event": "tracking.alert",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "acme/shop: 2 infrastructure changes on main",
"lines": ["New datastore: Redis", "Estimated cost: +€38 per month"],
"repo": "acme/shop",
"url": "https://brunel.cloud/c/…",
"data": {}
}id is stable: a redelivery sends the same body with the same id, so you can de-duplicate.
Verify the signature
Compute the HMAC-SHA256 of the raw body with your signing secret and compare it with the header in constant time, before parsing the JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string | null, secret: string): boolean {
const expected = Buffer.from(`sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`);
const got = Buffer.from(header ?? "");
return got.length === expected.length && timingSafeEqual(got, expected);
}import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")Delivery log and redelivery
Each delivery is logged with its attempts: time, duration, response code or error. Brunel makes one attempt per event; Redeliver sends the same body again, signed with the current secret, up to 10 attempts per delivery. Send a test event checks your endpoint. Deliveries are kept 30 days.
Rotate the signing secret
Rotate secret issues a new secret; later deliveries and redeliveries use it. Update your receiver first, then rotate.
Events
tracking.alert
Tracking alert. A push to a tracked branch changed the infrastructure, its cost or its security.
{
"id": "evt_Q2m9…",
"event": "tracking.alert",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "acme/shop: 2 infrastructure changes on main",
"lines": [
"New datastore: Redis",
"Estimated cost: +€38 per month"
],
"repo": "acme/shop",
"url": "https://brunel.cloud/c/abc123",
"data": {
"commit": "9f1c2e7",
"changes": 2
}
}pr.review
Pull request review. Brunel commented a pull request with its infrastructure, cost and security impact.
{
"id": "evt_Q2m9…",
"event": "pr.review",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "acme/shop#42 reviewed",
"lines": [
"Cost impact: +€12 per month",
"1 security finding"
],
"repo": "acme/shop",
"url": "https://github.com/acme/shop/pull/42",
"data": {
"pullRequest": 42
}
}deploy.pr
Deployment pull request. A pull request with Terraform and CI was opened in your repository.
{
"id": "evt_Q2m9…",
"event": "deploy.pr",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "Deployment pull request opened for acme/shop",
"lines": [
"Tier: growth",
"Provider: aws (eu-west-3)"
],
"repo": "acme/shop",
"url": "https://github.com/acme/shop/pull/43",
"data": {
"tier": "growth"
}
}price.report
Monthly price report. Costs recomputed with this month's provider catalogs, and the cheapest provider.
{
"id": "evt_Q2m9…",
"event": "price.report",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "Price report for acme/shop",
"lines": [
"growth: €212 per month (−€9)",
"Cheapest: scaleway"
],
"repo": "acme/shop",
"url": "https://brunel.cloud/c/abc123",
"data": {
"month": "2026-09"
}
}api_key.expiring
API key expiring. An API key with an expiry reminder expires in 7 days.
{
"id": "evt_Q2m9…",
"event": "api_key.expiring",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "API key “CI” expires in 7 days",
"lines": [
"Rotate or replace it in Console → API keys."
],
"repo": null,
"url": "https://brunel.cloud/console/keys",
"data": {
"keyId": "3f9a1c0b2d4e"
}
}test
Test event. Sent from the console to check your endpoint.
{
"id": "evt_Q2m9…",
"event": "test",
"createdAt": "2026-09-26T09:00:00.000Z",
"title": "Test event from the Brunel console",
"lines": [],
"repo": null,
"url": null,
"data": {}
}