Skip to content

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.

json
{
  "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.

json
{
  "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.

json
{
  "type": "call.done",
  "call_id": "call_abc",
  "node_id": "pbx_xyz",
  "status": "done",
  "duration_sec": 142
}

batch.status

ElevenLabs batch-call status update.

json
{
  "type": "batch.status",
  "call_id": "camp_parent",
  "batch_id": "batch_xyz",
  "status": "completed",
  "completed": 50,
  "total": 50
}

campaign.started / campaign.stopped

Campaign lifecycle events.

json
{
  "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.

json
{
  "type": "graph.version",
  "workspace_id": 42,
  "version": 137,
  "user_id": 1
}

Pings

json
{ "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:

json
{ "action": "subscribe", "channel": "calls:pbx_xyz" }

The server responds with:

json
{ "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:

  1. Wait with exponential backoff (e.g. 1 s, 2 s, 4 s, max 30 s).
  2. Reconnect with a fresh JWT if your token is close to expiry.
  3. 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

js
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
};

AiSpinner Documentation