Connect your systems

Realtime socket

The WebSocket the agent app uses: topics, sequence numbers, replay after a dropped connection, presence and typing. For dashboards and custom agent surfaces.

Last updated

Webhooks are for servers; the socket is for screens. GET /api/ws upgrades to a WebSocket authenticated by the session cookie (the widget uses its visitor token). You subscribe to topics one at a time and receive the same events the outbox publishes, plus presence and typing. Each subscription is authorised against the caller's permissions and visibility, and again after their role or memberships change.

TopicCarries
acct.{account}.conversationsThe conversation list for the whole workspace: created, updated, new messages. Each frame is filtered to the conversations the caller may see, so one subscription covers every inbox.
acct.{account}.inbox.{inbox}The same for one inbox (members of the inbox, or roles that see everything).
conv.{id}Everything about one conversation: messages, status, assignment, typing.
user.{id}Notifications for one member (only your own).
acct.{account}.presenceWho comes online or goes offline (presence.online, presence.offline); GET /api/presence gives the current list to start from.
visitor.{contact}The widget's own topic: its conversations, for a signed-in visitor.
socket.ts
const last: Record<string, { seq: number; epoch?: string }> = {}; // per topic, for resuming

function connect() {
  const ws = new WebSocket("wss://app.kommio.io/api/ws"); // the browser sends the session cookie
  const subscribe = (topic: string) =>
    ws.send(JSON.stringify({ type: "subscribe", topic, since: last[topic]?.seq ?? 0, epoch: last[topic]?.epoch }));

  ws.onopen = () => {
    ["acct.12.conversations", "acct.12.presence"].forEach(subscribe);
    setInterval(() => ws.send(JSON.stringify({ type: "ping" })), 30_000); // heartbeat, keeps you online
  };
  ws.onmessage = (m) => {
    const f = JSON.parse(m.data); // { type, topic, seq, epoch, kind, data, error }
    switch (f.type) {
      case "subscribed": last[f.topic] = { seq: f.seq, epoch: f.epoch }; break;
      case "event":      // kind: "conversation.updated", "message.created", "typing", "presence.online" and the rest of the event catalogue
        if (f.seq) last[f.topic] = { ...last[f.topic], seq: f.seq };
        render(f.kind, f.data);
        break;
      case "resync":     // the server could not replay what you missed: reload from the API
        reloadFromApi(f.topic);
        break;
      case "error":      // e.g. a topic you may not follow
        console.warn(f.topic, f.error);
        break;
    }
  };
  ws.onclose = () => setTimeout(connect, 1000); // resubscribes with since/epoch
}
connect();

Event frames on a topic carry a sequence number. When you subscribe again after a dropped connection, send since (the last sequence you saw) and the epoch from the subscribed frame; the server replays what you missed from its buffer (the last 1,000 events or five minutes per topic), or answers resync when it cannot, for example after a server restart, in which case reload from the API. A client that falls too far behind also gets resync rather than an unbounded backlog. To show that you are typing, send {"type": "typing", "topic": "conv.4821"} on a conversation you follow. Typing is live only and never replayed.

Every claim on these pages is covered by a test in the repository, and the API reference is generated from the same contract every server serves at /api/openapi. Corrections to [email protected].