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 type | Fires when |
|---|---|
on_wakeup_ready | An agent wakeup generates a proactive message |
on_diary_generated | The agent's diary entry is written |
on_personality_updated | A significant personality shift is detected |
on_recurring_event_due | A 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:
| Field | Type | Description |
|---|---|---|
event_type | string | The registered event type (e.g. on_wakeup_ready) |
agent_id | string | The agent that generated the message |
user_id | string | The target user |
generated_message | string | The agent's proactive message text |
check_type | string | Wakeup or reminder context label |
message_id | string | Stable 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).
| Method | Returns | Description |
|---|---|---|
register(eventType, opts) | WebhookRegisterResponse | Register or update a webhook URL for an event type. signing_secret is returned only on first creation. |
list() | WebhookListResponse | List all registered webhooks for this tenant |
delete(eventType) | void | Remove a webhook registration |
listDeliveryAttempts(eventType) | DeliveryAttemptsResponse | Inspect recent delivery history (status, response code, duration) |
rotateSecret(eventType) | WebhookRegisterResponse | Generate a new signing secret; old secret stays valid briefly to allow rotation |
registerForProject(projectId, eventType, opts) | WebhookRegisterResponse | Register a project-scoped webhook |
listForProject(projectId) | WebhookListResponse | List webhooks for a project |
deleteForProject(projectId, eventType) | void | Remove a project-scoped webhook |
listDeliveryAttemptsForProject(projectId, eventType) | DeliveryAttemptsResponse | Delivery history for a project webhook |
rotateSecretForProject(projectId, eventType) | WebhookRegisterResponse | Rotate 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 itTutorials
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
Notifications (Polling)
Fetch pending proactive messages for a user by polling the Notifications API — the simplest way to consume schedules, wakeups, and tenant-triggered events in a web or mobile client.
Advance Time
Simulate time passing for an agent — fire schedules, generate diaries, decay mood, and replay what would have happened without waiting real time.