Webhooks
Subscribe to SynthBoard events. Every delivery is HMAC-SHA256 signed. Failed deliveries retry up to 5 times over 5 hours. After 10 consecutive failures the subscription auto-pauses; re-enable in /mcp/console.
Subscribing#
Two ways:
- UI: go to /mcp/console, click Webhooks tab, New subscription.
- API:
POST /api/v1/webhookswith Bearer auth. The response returns the signing secret once — save it immediately.
curl https://synthboard.ai/api/v1/webhooks \
-H "Authorization: Bearer sb_live_..." \
-H "Content-Type: application/json" \
-d '{
"label": "Production session completions",
"url": "https://example.com/webhooks/synthboard",
"events": ["session.complete", "outcomes.ready"]
}'
# Response:
# {
# "ok": true,
# "subscription": { "id": "...", "label": "..." },
# "signing_secret": "kT8aB9c...", // SAVE THIS — shown once
# "verify_instructions": "..."
# }Delivery headers#
| Header | Purpose |
|---|---|
| X-SynthBoard-Event | Event name, e.g. session.complete |
| X-SynthBoard-Signature | HMAC-SHA256 of `${timestamp}.${body}`, hex-encoded, prefixed sha256= |
| X-SynthBoard-Timestamp | Unix seconds when the delivery was signed |
| X-SynthBoard-Delivery-Id | Unique per-delivery ID for idempotent receivers |
| User-Agent | SynthBoard-Webhooks/1.0 |
| Content-Type | application/json |
Signature verification#
Compute HMAC-SHA256(signing_secret, `${ts}.${raw_body}`), hex-encode, compare in constant time. Reject if timestamps older than 5 minutes (replay protection).
Node.js / Express
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// rawBody middleware — Express consumes the body once; capture raw for HMAC.
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf.toString(); } }));
app.post("/webhooks/synthboard", (req, res) => {
const sig = (req.header("X-SynthBoard-Signature") ?? "").replace("sha256=", "");
const ts = req.header("X-SynthBoard-Timestamp") ?? "";
const body = (req as any).rawBody as string;
if (!sig || !ts) return res.status(400).end();
// Replay protection
const tsNum = parseInt(ts, 10);
if (Math.abs(Date.now() / 1000 - tsNum) > 300) return res.status(401).end();
const expected = createHmac("sha256", process.env.SYNTHBOARD_WEBHOOK_SECRET!)
.update(`${ts}.${body}`)
.digest("hex");
const sigBuf = Buffer.from(sig);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).end();
}
// req.body is already parsed by express.json()
console.log("Event:", req.body.event, req.body.data);
res.status(200).end();
});Python / Flask
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["SYNTHBOARD_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/synthboard")
def hook():
sig = request.headers.get("X-SynthBoard-Signature", "").removeprefix("sha256=")
ts = request.headers.get("X-SynthBoard-Timestamp", "")
body = request.get_data().decode()
if not sig or not ts:
abort(400)
if abs(time.time() - int(ts)) > 300:
abort(401)
expected = hmac.new(SECRET, f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
# process request.json
return "", 200Event types#
session.complete
Session finished successfully with a synthesized recommendation.
{
"event": "session.complete",
"user_id": "<uuid>",
"correlation_id": "abc123",
"at": "2026-04-24T12:00:00.000Z",
"data": {
"session_id": "<uuid>",
"verdict": "Recommended: launch the free tier with 5-session/mo cap.",
"credits_charged": 120,
"session_url": "https://synthboard.ai/session/<uuid>"
}
}session.failed
Session errored out mid-run. Error code + message in data.
{
"event": "session.failed",
"user_id": "<uuid>",
"correlation_id": "abc123",
"at": "2026-04-24T12:00:00.000Z",
"data": {
"session_id": "<uuid>",
"error_code": "upstream_error",
"error_message": "Anthropic API returned 503"
}
}session.cancelled
Session was cancelled via synthboard.session.cancel. Reconciliation complete.
{
"event": "session.cancelled",
"data": {
"session_id": "<uuid>",
"credits_charged": 37,
"credits_refunded": 83
}
}session.continued
A follow-up round was triggered on an existing session.
{
"event": "session.continued",
"data": {
"session_id": "<uuid>",
"round_number": 2
}
}outcomes.ready
Session outcomes (action plan / report / memo) generated successfully.
{
"event": "outcomes.ready",
"data": {
"session_id": "<uuid>",
"format": "decision_memo",
"outcome_id": "<uuid>"
}
}Retries & breaker#
Success — any HTTP 2xx response marks the delivery complete.
Retry schedule — 1 min, 5 min, 15 min, 60 min, 240 min. Up to 5 attempts total.
Auto-pause — after 10 consecutive failed deliveries, the subscription pauses. Re-enable it in /mcp/console (fixes the breaker state + resumes deliveries).
Debugging deliveries#
Use GET /api/v1/webhooks/{id}/deliveries to fetch the last 100 delivery attempts for a subscription, with response codes and retry timestamps. Also visible in /mcp/console → Webhooks.