Docs
Webhooks and events.
A pipeline run takes minutes, not milliseconds. Subscribe instead of polling: register a signed callback once, or read the live stream for one campaign.
Events
- run.started
- run.completed
- run.failed
- drafts.ready
- message.ready
- message.approved
- push.completed
- * — everything.
Register an endpoint
curl -X POST https://sayintel.com/api/public/v1/webhooks \
-H "Authorization: Bearer $SAYINTEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://you.example/hooks/sayintel","events":["run.completed","drafts.ready"]}'
// The signing secret is returned ONCE. Store it before you move on.Verify a delivery
Every delivery carries a signature header. Compute the HMAC over `${t}.${rawBody}` using the raw body, compare in constant time, and reject anything older than five minutes.
X-Sayintel-Signature: t=1770000000,v1=9f2c...
import { createHmac, timingSafeEqual } from "crypto";
function verify(secret: string, header: string, rawBody: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!Number.isFinite(age) || age > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}Retries
Answer 2xx quickly, and do the work after. Failed deliveries retry with backoff at roughly 30s, 2m, 10m, 30m, 2h and 6h, then stop. Inspect what happened with list_webhook_deliveries.
Live stream
For a UI that watches one run, server-sent events beat both polling and webhooks.
GET https://sayintel.com/api/public/v1/campaigns/<campaign_id>/events
Authorization: Bearer $SAYINTEL_TOKEN
event: run.progress
data: {"stage":"drafting","done":41,"total":120}
event: run.completed
data: {"run_id":"...","status":"done"}