sayintel
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"}