Skip to main content
SONZAI

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.

Proactive messages — generated by recurring schedules, one-off wakeups, or tenant-triggered events — land in a per-user notifications queue the moment they fire. Your frontend or backend polls that queue to fetch pending messages, display them to the user, and mark each one consumed. No push infrastructure, no webhook endpoint, no server-side listener to maintain — just an HTTP GET on your schedule.

This is the recommended delivery pattern for web clients and mobile apps that can't accept inbound HTTP requests, and it doubles as a handy catch-up mechanism for users who were offline when messages were generated.

What you can build with it

  • Mobile app inbox — periodic fetch of pending messages on foreground, display as native notifications or in-app banners
  • Web dashboard — a dedicated tab showing all of the agent's pending outreach to a given user
  • Delayed delivery — let messages queue up while the user is offline; deliver the full batch on reconnect
  • Audit / moderation — preview agent-generated messages before pushing to downstream delivery (email, SMS)
  • Development / testing — poll notifications during a test run to verify that schedules and wakeups fired correctly

Quickstart

Poll for pending messages, display the latest, then mark each one consumed.

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

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

const pending = await client.agents.notifications.list("agent_abc", {
user_id: "user_123",
limit: 10,
});

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

Core concepts

Queue semantics

When a proactive message fires — whether from a schedule, a wakeup, or a trigger event — the platform enqueues it for the relevant user. The queue is per-user, per-agent. Calling list returns only messages in pending state; calling consume transitions a specific message to consumed. Consumed messages are excluded from future list responses but remain visible in history. The queue does not auto-expire: messages stay pending indefinitely until your code marks them consumed.

Polling cadence

There is no hard requirement on how frequently you poll, but these guidelines work well in practice:

  • Foreground (user has the app open): every 10–30 seconds
  • Background (app backgrounded or tab hidden): every 2–5 minutes, or on visibility-change events
  • Reconnect burst: poll immediately when the user comes back online after a period of inactivity, then resume normal cadence

Avoid polling more frequently than every 10 seconds — there is no benefit since notification generation is event-driven, not continuous.

Vs SSE (live chat stream)

If the user has an active SSE chat stream open, proactive messages appear inline in the conversation automatically — no polling needed. Polling is the catch-up mechanism for users who do not have a live stream. The two patterns are complementary: SSE for foreground delivery, polling for background or offline users.

History endpoint

notifications.history is separate from notifications.list. It returns all historical notifications for an agent (including already-consumed ones) and is useful for audit trails, moderation dashboards, and debugging. It does not filter by user_id — it returns across all users up to the requested limit.

Full API

All methods are on client.agents.notifications.* (TS/Python) or client.Agents.Notifications (Go). Full request and response shapes live in the API reference.

MethodSignatureReturnsDescription
listlist(agentId, { user_id?, limit? }){ notifications: Notification[] }Fetch pending messages for a user
consumeconsume(agentId, messageId)voidMark a single message consumed
historyhistory(agentId, limit){ notifications: Notification[] }Fetch all historical notifications (consumed + pending)

Notification fields

FieldJSON keyDescription
MessageIDmessage_idPass this to consume to mark the message delivered
UserIDuser_idThe user this notification was generated for
CheckTypecheck_typeThe check type (e.g. "reminder", "interest_check", "birthday")
GeneratedMessagegenerated_messageThe actual text the agent produced — display this to the user
CreatedAtcreated_atWhen the message was enqueued (RFC 3339 UTC)
ScheduleIDschedule_idSet if the message originated from a schedule; otherwise absent
WakeupIDwakeup_idSet if the message originated from a wakeup; otherwise absent

Use the correct field names

Older code may use id, notificationId, type, or content. These are incorrect. The canonical fields are message_id, check_type, and generated_message. Using the wrong field names will result in silent failures when calling consume.

Combines with

With Scheduled Reminders — delivery side of recurring messages

A schedule defines when the agent fires; polling is one way to receive what it produced. When a schedule's cadence fires, the platform generates the agent's message and enqueues it. Your client polls, displays generated_message, then calls consume to clear it from the queue. The schedule and delivery are fully decoupled — you can swap in webhooks or SSE without touching the schedule definition.

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

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

// 1. Create a daily 09:00 check-in schedule (done once, e.g. at onboarding)
await client.schedules.create("agent_abc", "user_123", {
cadence: {
  simple: { frequency: "daily", times: ["09:00"] },
  timezone: "Asia/Singapore",
},
intent: "morning check-in on mood and sleep",
check_type: "reminder",
});

// 2. On each app foreground, poll for what the schedule produced
const pending = await client.agents.notifications.list("agent_abc", {
user_id: "user_123",
limit: 5,
});

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

With Wakeups — delivery side of one-off check-ins

A wakeup fires once at a specific moment; polling retrieves the message it generated. This is the natural delivery pattern for one-off agent outreach in mobile clients where webhooks are unavailable. Schedule the wakeup when the event is known (e.g. "follow up 24 hours after purchase"), then poll periodically — the message lands in the queue the moment the delay elapses.

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

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

// 1. Schedule a one-off wakeup (e.g. after a user completes onboarding)
await client.agents.scheduleWakeup("agent_abc", {
user_id:     "user_123",
check_type:  "interest_check",
intent:      "check in about how onboarding went",
delay_hours: 24,
});

// 2. Poll for the message when it fires (24 h later)
const pending = await client.agents.notifications.list("agent_abc", {
user_id: "user_123",
limit: 5,
});

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

With Webhooks — alternative push delivery

Polling and webhooks are two delivery patterns for the same underlying notifications queue. Choose based on your infrastructure:

  • Polling — your client asks the server for new messages on a schedule. Simple to implement, works in browsers and mobile apps, no inbound connectivity required. Latency is bounded by your polling interval.
  • Webhooks — the server pushes each message to a URL you register the moment it fires. Lower latency, better for server-to-server integration and multi-channel fanout (email, SMS, push notifications). Requires a public HTTPS endpoint to receive callbacks.

You can use both simultaneously: poll from mobile clients for in-app delivery and register a webhook on your backend for email/SMS fanout. The queue tracks consumed state per message, so a message consumed via polling will not appear in webhook delivery (and vice versa).

Tutorials

  • Medication Reminders — full-stack example combining Schedule + Inventory + Memory; shows the end-to-end flow from schedule creation to polling the generated reminder.
  • Scheduled Reminders — reference walkthrough — covers cadence shapes, DST, preview, and the full lifecycle including how fired messages appear in the notifications queue.

Next steps

On this page