Realtime Events (WebSocket)
AiSpinner pushes real-time events over a single WebSocket connection per workspace. This replaces HTTP polling for call events, batch status, journal updates, graph version notifications, and worker telemetry.
Connection
wss://api.aispinner.io/ws/events/{workspace_id}?token=<JWT>- One connection per workspace
- Auto-reconnect on disconnect (clients are expected to back off and retry)
- Server pings every 30 s; clients should pong (or the underlying WS lib should handle it)
Client library
The Flutter app uses a single EventsClient per workspace and registers multiple listeners by nodeId, so blocks can independently subscribe to the events they care about without re-opening sockets.
Message Format
All messages are JSON. Each has a type field that indicates the event kind, plus event-specific payload fields.
{
"type": "<event_type>",
"...": "..."
}Event Types
journal.new
A new call (or batch of calls) has been recorded for the workspace. Frontend uses this to incrementally append to the Journal block without polling.
{
"type": "journal.new",
"entries": [
{
"id": 12345,
"call_id": "call_abc",
"node_id": "pbx_xyz",
"status": "done",
"subscriber_phone": "+1555...",
"duration_sec": 142,
"created_at": "2026-05-04T10:23:45Z"
}
]
}call.done / call.transcript
Single-call lifecycle events. call.done fires when a call completes (with final status); call.transcript fires when a transcript is finalised.
{
"type": "call.done",
"call_id": "call_abc",
"node_id": "pbx_xyz",
"status": "done",
"duration_sec": 142
}batch.status
ElevenLabs batch-call status update.
{
"type": "batch.status",
"call_id": "camp_parent",
"batch_id": "batch_xyz",
"status": "completed",
"completed": 50,
"total": 50
}campaign.started / campaign.stopped
Campaign lifecycle events.
{
"type": "campaign.started",
"call_id": "camp_parent",
"node_id": "pbx_xyz"
}graph.version
The workspace graph has been saved (typically by another tab or another collaborator). Clients use this to refresh their local graph state and avoid version conflicts.
{
"type": "graph.version",
"workspace_id": 42,
"version": 137,
"user_id": 1
}Pings
{ "type": "ping" }
{ "type": "pong" }Other Telephony Events
The hub also forwards lower-level call events such as call state transitions, DTMF, voicemail-detected, and interruption signals. Clients subscribe to a generic telephony listener to receive these.
Subscriptions
The /ws/events/{workspace_id} channel is auto-subscribed for the workspace. The client may explicitly subscribe to additional channels if needed:
{ "action": "subscribe", "channel": "calls:pbx_xyz" }The server responds with:
{ "type": "subscribed", "channel": "calls:pbx_xyz" }Authentication & Authorisation
- The JWT in the query string is verified on connection. Invalid or expired tokens close the socket immediately.
- A workspace member or operator can connect; non-members receive a
403. - The connection is bound to the JWT's
uid. Sending events on behalf of another user is rejected.
Disconnect Handling
If the WebSocket closes for any reason, you should:
- Wait with exponential backoff (e.g. 1 s, 2 s, 4 s, max 30 s).
- Reconnect with a fresh JWT if your token is close to expiry.
- After reconnect, fetch the latest graph version and any deltas you missed (e.g. via
GET /telephony/journal/{node_id}?since_id=<last_seen>).
Example: minimal JS client
const ws = new WebSocket(
`wss://api.aispinner.io/ws/events/${workspaceId}?token=${jwt}`
);
ws.onmessage = (msg) => {
const event = JSON.parse(msg.data);
switch (event.type) {
case 'journal.new':
console.log('New entries:', event.entries);
break;
case 'call.done':
console.log('Call finished:', event.call_id);
break;
case 'graph.version':
console.log('Graph saved to v', event.version);
break;
}
};
ws.onclose = () => {
// reconnect after backoff
};