> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brainstormer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build Webhook

> Out-of-band, HMAC-signed delivery of the terminal ProvisioningEvent to your deliver.webhookUrl. Powers email-me-a-link UX without holding a connection.

# Build Webhook

When you set `deliver.webhookUrl` on
[`from-url`](/api-reference/provisioning/from-url), the platform **POSTs**
lifecycle events to that URL — at minimum the **terminal** event (`ready` or
`failed`). This powers "email-me-a-link" UX without the client holding an SSE
connection. The body is a
[`ProvisioningEvent`](/api-reference/provisioning/build-events-sse#provisioningevent-contract)
and is **HMAC-signed** so you can verify authenticity.

## Delivery

| Property                   | Value                                                       |
| -------------------------- | ----------------------------------------------------------- |
| Method                     | `POST`                                                      |
| Body                       | JSON-encoded `ProvisioningEvent` (same shape as SSE / poll) |
| `Content-Type`             | `application/json`                                          |
| `X-Brainstormer-Signature` | `sha256=<hmac>` over the **raw** JSON body                  |
| `X-Brainstormer-Event`     | The event `status` (e.g. `ready`, `failed`)                 |
| Retries                    | 3 attempts with backoff on any non-2xx response             |

The HMAC is **HMAC-SHA256** over the raw request body, keyed with the
**platform's webhook secret** (provisioned with your API key / org). Respond
`2xx` quickly to acknowledge; non-2xx triggers a retry.

## Example Payload

```http theme={null}
POST /api/hook HTTP/1.1
Host: yourapp.com
Content-Type: application/json
X-Brainstormer-Event: ready
X-Brainstormer-Signature: sha256=3a7bd3e2360a3d7...

{
  "buildId": "bld_8c9d0e1f2a3b",
  "status": "ready",
  "progress": 100,
  "step": "ready",
  "stepLabel": "Your agent is ready",
  "platform": "youtube",
  "channelTitle": "The Channel",
  "agentSlug": "the-channel-agent",
  "starterQuestions": [
    "What videos have you made about productivity?",
    "Summarize your latest upload"
  ],
  "emittedAt": "2026-06-14T01:38:10.221Z"
}
```

## Verifying the Signature (Node / Express)

Always verify the signature **against the raw body** before trusting the payload.
Use a constant-time comparison.

```typescript Node / Express theme={null}
import express from "express";
import crypto from "node:crypto";

const WEBHOOK_SECRET = process.env.BRAINSTORMER_WEBHOOK_SECRET!;
const app = express();

// IMPORTANT: capture the RAW body — HMAC must be computed over exact bytes.
app.use(
  "/api/hook",
  express.raw({ type: "application/json" }),
);

function verify(rawBody: Buffer, signatureHeader: string | undefined): boolean {
  if (!signatureHeader?.startsWith("sha256=")) return false;
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(rawBody)
    .digest("hex");
  const provided = signatureHeader.slice("sha256=".length);
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(provided, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/api/hook", (req, res) => {
  const raw = req.body as Buffer; // Buffer from express.raw
  if (!verify(raw, req.header("X-Brainstormer-Signature"))) {
    return res.status(401).send("invalid signature");
  }

  const event = JSON.parse(raw.toString("utf8"));
  if (event.status === "ready") {
    // e.g. email the user a link to chat at event.agentSlug
    console.log("Agent ready:", event.agentSlug);
  } else if (event.status === "failed") {
    console.error("Build failed:", event.error?.code, event.error?.message);
  }

  res.status(200).send("ok"); // ack fast; non-2xx triggers a retry
});

app.listen(3000);
```

## curl (compute a test signature)

```bash curl theme={null}
BODY='{"buildId":"bld_test","status":"ready"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$BRAINSTORMER_WEBHOOK_SECRET" | sed 's/^.* //')

curl -X POST https://yourapp.com/api/hook \
  -H "Content-Type: application/json" \
  -H "X-Brainstormer-Event: ready" \
  -H "X-Brainstormer-Signature: sha256=$SIG" \
  -d "$BODY"
```

<Warning>
  Compute the HMAC over the **raw bytes** of the request body — re-serializing
  parsed JSON (different key order or whitespace) will change the digest and fail
  verification. Capture the raw body before any JSON middleware parses it.
</Warning>

## Status Values & Error Model

The webhook body is a `ProvisioningEvent` — see the full
[`ProvisioningStatus`](/api-reference/provisioning/build-events-sse#provisioningstatus-values)
and
[`ProvisioningErrorCode`](/api-reference/provisioning/build-events-sse#provisioningerrorcode-table)
references. On a `failed` event, branch on `error.retryable`.
