Skip to main content
SONZAI

Webhooks

Receive proactive agent messages at a URL you control — signed webhook delivery for push notifications, email/SMS fanout, and server-to-server integrations.

Register a webhook URL per tenant (or per project) and Sonzai will HTTP POST every proactive agent message to that URL with a signed payload. Each request includes a Sonzai-Signature header you verify with your signing secret before acting on the payload. Use webhooks for server-to-server delivery where you own the downstream routing — forwarding to FCM/APNs, sending via SendGrid or Twilio, writing to a case-management system, or fanning out to multiple channels at once.

What you can build with it

  • Push notifications — webhook handler forwards the agent message to FCM (Android) or APNs (iOS)
  • Email / SMS fanout — webhook handler sends through SendGrid, Postmark, Twilio, or any provider you already use
  • Multi-channel delivery — fan a single agent message to two or more user channels in one handler
  • Downstream analytics — log and inspect every proactive message before it reaches the user
  • Enterprise integrations — route agent messages into Slack, Microsoft Teams, internal tooling, or CRM workflows

Quickstart

Register a webhook URL to start receiving on_wakeup_ready events. Save the signing_secret from the response — it is only returned once.

import { Sonzai } from "@sonzai-labs/agents";

const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

const result = await client.webhooks.register("on_wakeup_ready", {
webhookUrl: "https://your-server.com/webhooks/sonzai",
authHeader: "Bearer your-webhook-secret",
});

// Store this securely — shown only once
console.log(result.signingSecret);

Core concepts

Registration

Webhooks are registered per event type. One URL per event type per tenant, or per project when using project-scoped registration. The same URL can handle multiple event types — inspect the event_type field on the payload to route accordingly.

Available event types:

Event typeFires when
on_wakeup_readyAn agent wakeup generates a proactive message
on_diary_generatedThe agent's diary entry is written
on_personality_updatedA significant personality shift is detected
on_recurring_event_dueA scheduled reminder fires

Signed payload

Every POST Sonzai sends includes a Sonzai-Signature header in the format:

Sonzai-Signature: t=1714000000,v1=abc123def456...

t is the Unix timestamp of the request; v1 is the HMAC-SHA256 of {timestamp}.{raw_body} using your signing secret (with the whsec_ prefix stripped). Always verify the signature on the raw, unmodified request body before parsing JSON — do not use the parsed object for verification.

Retries

When your endpoint returns a non-2xx status or times out, Sonzai retries with exponential backoff. Make your handler idempotent — deduplicate on event_id (or a stable field in the payload body) so retried deliveries do not double-process.

Payload shape

The webhook body matches the Notification shape returned by the polling API. Key fields:

FieldTypeDescription
event_typestringThe registered event type (e.g. on_wakeup_ready)
agent_idstringThe agent that generated the message
user_idstringThe target user
generated_messagestringThe agent's proactive message text
check_typestringWakeup or reminder context label
message_idstringStable ID; use for deduplication

Signature verification

Verify the Sonzai-Signature header before acting on any payload. The Go SDK ships a helper; TypeScript and Python use standard crypto primitives.

import crypto from "node:crypto";

/**
* Verify a Sonzai webhook signature.
* Call this on the raw request body string before parsing JSON.
*/
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// Strip whsec_ prefix if present
const key = secret.startsWith("whsec_") ? secret.slice(6) : secret;

// Parse header: t={timestamp},v1={sig}
const parts = Object.fromEntries(
  signatureHeader.split(",").map((p) => p.split("=")),
);
const timestamp = parts["t"];
const receivedSig = parts["v1"];
if (!timestamp || !receivedSig) return false;

const expectedSig = crypto
  .createHmac("sha256", key)
  .update(`${timestamp}.${rawBody}`)
  .digest("hex");

return crypto.timingSafeEqual(
  Buffer.from(receivedSig),
  Buffer.from(expectedSig),
);
}

// In your webhook handler (e.g. Express):
app.post("/webhooks/sonzai", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["sonzai-signature"] as string;
const rawBody = req.body.toString("utf-8");

if (!verifyWebhookSignature(rawBody, sig, process.env.SONZAI_WEBHOOK_SECRET!)) {
  return res.status(401).send("Invalid signature");
}

const event = JSON.parse(rawBody);
// Forward to your channel...
res.status(200).send("ok");
});

Timestamp tolerance

The Go SDK rejects signatures older than 5 minutes by default. In TypeScript and Python implementations, add a timestamp check if you need to guard against replay attacks: compare parseInt(parts["t"]) * 1000 against Date.now() and reject if the difference exceeds 300 000 ms.

Full API

All methods are on client.webhooks (TS/Python) or client.Webhooks (Go).

MethodReturnsDescription
register(eventType, opts)WebhookRegisterResponseRegister or update a webhook URL for an event type. signing_secret is returned only on first creation.
list()WebhookListResponseList all registered webhooks for this tenant
delete(eventType)voidRemove a webhook registration
listDeliveryAttempts(eventType)DeliveryAttemptsResponseInspect recent delivery history (status, response code, duration)
rotateSecret(eventType)WebhookRegisterResponseGenerate a new signing secret; old secret stays valid briefly to allow rotation
registerForProject(projectId, eventType, opts)WebhookRegisterResponseRegister a project-scoped webhook
listForProject(projectId)WebhookListResponseList webhooks for a project
deleteForProject(projectId, eventType)voidRemove a project-scoped webhook
listDeliveryAttemptsForProject(projectId, eventType)DeliveryAttemptsResponseDelivery history for a project webhook
rotateSecretForProject(projectId, eventType)WebhookRegisterResponseRotate signing secret for a project webhook

WebhookEndpoint fields: event_type, webhook_url, auth_header, is_active, created_at.

WebhookDeliveryAttempt fields: attempt_id, event_type, webhook_url, response_code, response_body, error_message, duration_ms, attempt_number, status, created_at.

Combines with

With Notifications polling — alternative consumption model

Webhooks and polling are two consumption models for the same proactive message queue. Webhooks push to your server in real time; polling lets your client or server fetch on demand. Use webhooks when you have a stable server endpoint and need instant delivery. Use polling when your client cannot accept inbound HTTP connections (mobile apps, browser clients) or when you want to batch-process notifications on your own schedule. Both see the same payload shape.

// Polling alternative — same messages, pulled instead of pushed
const pending = await client.agents.notifications.list("agent_abc", {
  userId: "user_123",
  status: "pending",
});

for (const notif of pending.notifications) {
  console.log(notif.generated_message);
  await client.agents.notifications.consume("agent_abc", notif.message_id);
}

With Scheduled Reminders — fan recurring reminders to channels

When a scheduled reminder fires, an on_recurring_event_due webhook delivers the generated message to your endpoint. Your handler can then forward to FCM, send an email, or post to Slack — all without polling. This separates the scheduling concern (when to fire) from the delivery concern (how to reach the user).

// Register once; every scheduled reminder fires this endpoint
const result = await client.webhooks.register("on_recurring_event_due", {
  webhookUrl: "https://api.yourapp.com/webhooks/sonzai",
});

// In your handler, forward to the appropriate channel:
// event.generated_message → FCM, email, SMS, Slack...

With Wakeups — push one-off check-ins

When a wakeup fires, the on_wakeup_ready event is POSTed to your registered endpoint. This is the primary webhook event for companion-style agents that reach out proactively. Register the webhook once and every future wakeup — automatic or manually scheduled — will arrive at your URL.

// Register to receive all future wakeup messages
await client.webhooks.register("on_wakeup_ready", {
  webhookUrl: "https://api.yourapp.com/webhooks/sonzai",
});

// Your handler receives the wakeup message and forwards it:
// event.generated_message → push notification
// event.user_id          → lookup device token in your DB
// event.agent_id         → identify which agent sent it

Tutorials

No dedicated webhook tutorial yet. The Scheduled Reminders tutorial covers the full proactive delivery pipeline and includes webhook-based consumption patterns.

Next steps

  • Notifications polling — pull-based alternative for clients that cannot receive inbound HTTP
  • Scheduled Reminders — recurring proactive messages that fire over webhooks
  • Wakeups — one-off proactive messages delivered via on_wakeup_ready

On this page