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

webhook-create.ts
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 manager

Event 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.

verify.ts
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

Every event carries a stable `id`. Use it as the dedup key in your handler, at-least-once delivery means you will see duplicates under network failure.
Was this page helpful?