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 stream extra: pip install 'agentboxd[stream]'. The key needs the messages:read permission. Without drafts:read it gets draft events as envelopes (no draft body).
#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 frame or subscription: invalid_json or invalid_message (a malformed frame), invalid_subscription (unknown event type), inbox_not_found, or rate_limited (since more than once in 5 seconds). The previous subscription stays. If you get a code that isn’t listed, reconnect with since. |
Event types are the webhook ones: message.received, message.sent, message.delivered, message.bounced, message.complained, message.enriched, message.released, message.received.blocked, inbox.expired, inbox.paused, inbox.resumed, domain.dkim_rotated, domain.dkim_rotation_due, domain.verified, domain.failed, connector.authorized and connector.revoked (the hosted MCP connector), attachment.extracted and attachment.extraction_failed (document extraction), identity.token_issued and identity.signed_in, signup.created and signup.claimed, workspace.stopped and workspace.resumed (the emergency stop), member.invited, member.invite_revoked, member.joined, member.role_changed and member.removed (your team), suppression.removed (your suppression list), escalation.sent (human on call), and the drafts events draft.created, draft.updated, draft.sent, draft.failed and draft.cancelled. See the event catalog for example bodies.
An inbox-scoped key gets the events of its inbox, live and on resume. Events about the whole workspace go only to workspace keys: team, domain, emergency stop and suppression list events, connector events of a connection to every inbox, and escalations that are not about one inbox.
#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 and 4 MB of event data. replay_truncated: true means some may be missing (the id is older than that, unknown, or more than that 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 stream that closes during its replay counts until the replay ends.
- A client that stops reading while more than 1 MiB waits for it is dropped (4009); reconnect with
since. - Frames are answered one at a time; more than 8 waiting for a reply close the connection (1008).
- A revoked key or an ended session closes the stream within two minutes (4001). So does switching the session to another workspace, or leaving or being removed from the workspace.
| Code | Meaning | What to do |
|---|---|---|
1001 | Server restarting | Reconnect with since |
1008 | Too many frames sent without waiting for replies | Wait for each reply before sending more |
1011 | Server error | Wait a moment, then 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 |