Webhooks
Webhooks are how you stream Saaya events into your stack. They are signed, retried with exponential backoff, and delivered at-least-once, which means your handler must be idempotent.
Subscribing
const hook = await saaya.webhooks.create({
url: "https://api.acme.com/saaya/events",
events: [
"session.started",
"session.ended",
"tool.errored",
"campaign.row.completed",
"agent.published",
],
});
console.log(hook.signingSecret); // store this in your secret managerEvent types
See the full Webhook events catalog for the complete list. The most common are `session.started`, `session.ended`, `tool.errored`, `campaign.row.completed`, and `agent.published`. Every event has a stable schema and a `version` field for forward-compat.
Signature verification
Saaya signs every payload with HMAC-SHA256 using the signing secret returned at creation. The signature ships in the `Saaya-Signature` header along with a timestamp; reject any request older than 5 minutes.
import crypto from "node:crypto";
export function verify(rawBody: string, header: string, secret: string): boolean {
const [tsPart, sigPart] = header.split(",");
const ts = tsPart.split("=")[1];
const sig = sigPart.split("=")[1];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Retries
A non-2xx response triggers a retry. Saaya retries up to 8 times over ~24 hours with exponential backoff (1m, 5m, 30m, 2h, 6h, 12h, 24h, give-up). Failed deliveries land in a dead-letter queue you can replay from the dashboard.
Idempotency