Docs · Guides
Realtime events
The webhook events, pushed to your agent over a WebSocket: no public URL, reconnect and resume built in.
The event stream sends the same events as webhooks, with the same bodies and ids, over one WebSocket your agent opens. It needs no public URL, so it works from a laptop, a job behind NAT or a browser tab. The SDKs reconnect on their own and pick up where they left off.
#With the SDKs
import { Agentboxd } from 'agentboxd';
const mr = new Agentboxd();
for await (const event of mr.stream({ inboxIds: [inbox.id], eventTypes: ['message.received'] })) {
const { message } = event.data as { message: { from: string; subject: string } };
console.log(event.id, message.from, message.subject);
}import asyncio
from agentboxd import AsyncAgentboxd
async def main() -> None:
async with AsyncAgentboxd() as mr:
async for event in mr.stream(event_types=["message.received"]):
print(event["id"], event["data"]["message"]["subject"])
asyncio.run(main())TypeScript uses the built-in WebSocket (browsers, Node 22+; on Node 20 run npm install ws). Python needs the extra: pip install 'agentboxd[stream]'. The key needs the messages:read permission.
#Connecting
Open wss://api.agentboxd.com/v1/stream (GET /v1/stream with a WebSocket upgrade) and authenticate in one of two ways:
- Header:
Authorization: Bearer mr_…on the upgrade request. For servers and CLIs. - Stream token:
POST /v1/stream/tokenreturns{ token, expires_at, url }; connect to thaturl(?token=st_…). The token works once and expires after 60 seconds, so it can sit in a URL. For browsers, which can’t set headers. It keeps the key’s scope: a token from an inbox-scoped key only sees that inbox.
# Any WebSocket client: send the key in the Authorization header of the upgrade.
npx wscat -c wss://api.agentboxd.com/v1/stream -H "Authorization: Bearer $AGENTBOXD_API_KEY"
< {"type":"hello","connection_id":"…","scope":{"inbox_id":null},"heartbeat_seconds":30,"replay":{"max_events":1000,"max_age_seconds":3600}}
> {"type":"subscribe","event_types":["message.received"]}
< {"type":"subscribed","inbox_ids":null,"event_types":["message.received"],"payload":"full","replayed":0,"replay_truncated":false}
< {"type":"event","event":{"id":"…","type":"message.received","created_at":"…","data":{"inbox":{…},"thread_id":"…","message":{…}}}}// Server side (keeps the key): mint a single-use URL, valid for 60 seconds.
const { url } = await mr.streamToken(); // POST /v1/stream/token
// Browser: no key, no headers.
const ws = new WebSocket(url);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'hello') ws.send(JSON.stringify({ type: 'subscribe', payload: 'envelope' }));
if (msg.type === 'event') console.log(msg.event.type, msg.event.data.subject);
};#Messages
Every frame is a JSON object with a type. After hello, send one subscribe; sending another replaces it.
| Message | Meaning |
|---|---|
→ { type: "subscribe", inbox_ids?, event_types?, payload?, since? } | Filters (omitted = everything the key can see), payload: "full" (default) or "envelope" (ids, addresses, subject, labels; no bodies), and since to replay missed events first. |
→ { type: "unsubscribe" } · { type: "ping" } | Stop events · keepalive, answered with { type: "pong" }. |
← hello | connection_id, the key’s scope, heartbeat_seconds and the replay window. |
← subscribed | The active filters, replayed (events resent from since) and replay_truncated. |
← { type: "event", event } | event is exactly the webhook body: { id, type, created_at, data }. |
← { type: "error", code, message } | A refused subscription: invalid_subscription (unknown event type) or inbox_not_found. The previous subscription stays. |
Event types are the webhook ones: message.received, message.sent, message.delivered, message.bounced, message.complained, message.enriched, message.received.blocked, inbox.expired, and the drafts events draft.created, draft.updated, draft.sent, draft.failed and draft.cancelled. See the event catalog for example bodies.
#Resume after a disconnect
Remember the id of the last event you handled and subscribe with since: <that id>. The server first sends what you missed, in order, then live events, with no gap and no duplicates. It keeps the last hour, up to 1000 events. replay_truncated: true means some may be missing (the id is older than that, unknown, or more than 1000 events followed it); fetch recent messages with the REST API to catch up. The SDKs do all of this for you.
#Limits and close codes
- The server pings every 30 seconds and drops connections that stop answering.
- Subscribe within 60 seconds of connecting, or the connection is closed (4008).
- Up to 20 open streams per workspace (4029 beyond that).
- A client that stops reading while more than 1 MiB waits for it is dropped (4009); reconnect with
since. - A revoked key or an ended session closes the stream within two minutes (4001).
| Code | Meaning | What to do |
|---|---|---|
1001 | Server restarting | Reconnect with since |
4001 | Missing, invalid, expired or revoked credential; or a key in the URL | Stop; get a new key or token |
4003 | The key lacks messages:read | Stop |
4008 | No subscription within 60 s | Subscribe right after hello |
4009 | Slow consumer | Reconnect with since, read faster |
4029 | Too many streams for this workspace | Close some; reconnect later |