Documentation
Conversational Triage API
Build your solution
Realtime (WebSocket)

Realtime (WebSocket)

wss://api.infermedica.com/api/ct/v2/realtime

The WebSocket transport provides a live conversation session with incremental assistant output and conversation-level state updates over a single long-lived connection. It exposes the same conversation model as HTTP v2 — a conversation created over WebSocket can be read over HTTP and vice versa.

Use it for voice channels, for interruption handling, and for sessions where reconnect-and-resume matters more than request/response simplicity.

Authentication

Send the bearer token in the Authorization header when establishing the connection. If authentication fails before the upgrade completes, the server closes the socket with code 1008 (policy violation) — there is no HTTP error body to read.

Connection lifecycle

  1. Connect with a valid Authorization header. The server accepts the upgrade.
  2. Send conversation.create or conversation.resume as the first frame.
  3. Receive conversation.created or conversation.resumed with the full conversation snapshot.
  4. Exchange live conversation events. The server sends ping every 30 seconds; reply with pong.
  5. Either side may close the socket. Reconnect later with conversation.resume.

Handshake failures are explicit:

SituationWhat the server does
First frame that is not a valid conversation.create or conversation.resume — unparseable, or failing field validationerror with code invalid_payload, then close 1007.
A parseable first frame of any other typeerror with code invalid_message_type, then a normal 1000 close.
Resuming a conversation that does not existerror with code conversation_not_found, then close 1000.
conversation.create seeding evidence the knowledge base does not knowerror with code unknown_evidence, then close 1000.
Connection rejected by the rate limitererror with code rate_limit_exceeded, then close 1008.

Client events

Only conversation.create and conversation.resume are valid as the first frame. After the handshake you may send conversation.user_message.create, conversation.settings.update, and pong.

conversation.create

Opens a new conversation. conversation is a ConversationInput — the same shape as the HTTP create body.

JSON
{
  "type": "conversation.create",
  "conversation": {
    "settings": {
      "language": "en",
      "channel": "text",
      "ui_widgets_enabled": false,
      "opening_message_enabled": false
    },
    "assessment": {
      "age": { "value": 35, "unit": "year" },
      "sex": "female",
      "relationship": "self",
      "evidence": []
    }
  }
}

conversation.resume

Resumes an existing conversation by id.

JSON
{
  "type": "conversation.resume",
  "conversation_id": "68d9a84d-711c-4a55-a9dd-da6363e58e38"
}

conversation.user_message.create

Creates a user message and starts a turn. user_message is a UserMessageInput.

JSON
{
  "type": "conversation.user_message.create",
  "user_message": {
    "id": "68d9a84d-711c-4a55-a9dd-da6363e58e38",
    "content": "I have a headache",
    "is_interrupted": true,
    "played_content": "How long have you had the",
    "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
  }
}

When the user did not interrupt, omit is_interrupted and played_content — or set is_interrupted to false and omit played_content.

is_interrupted and played_content are a record of what the user actually heard, not a control flag: they tell the engine how much of the previous reply landed. Preemption happens because of the transport, not because of these fields — sending a message mid-turn preempts the active turn whether or not you set them.

conversation.settings.update

Partial settings update; omitted fields keep their current value.

JSON
{
  "type": "conversation.settings.update",
  "settings": { "language": "pl", "channel": "voice" }
}

pong

Reply to the server's ping.

JSON
{ "type": "pong" }

Server events

The server emits the shared streaming events — the same payloads as HTTP NDJSON — plus four transport-only events documented here.

conversation.created

Acknowledges conversation.create. Always the first server event after a successful create handshake.

JSON
{
  "type": "conversation.created",
  "conversation": {
    "id": "68d9a84d-711c-4a55-a9dd-da6363e58e38",
    "status": "in_progress",
    "settings": { "...": "..." },
    "assessment": { "...": "..." },
    "messages": [],
    "is_terminated": false
  }
}
FieldTypeDescription
conversationConversationFull snapshot of the newly created conversation.

conversation.resumed

Acknowledges conversation.resume with the full snapshot, including history.

JSON
{
  "type": "conversation.resumed",
  "conversation": {
    "id": "68d9a84d-711c-4a55-a9dd-da6363e58e38",
    "status": "in_progress",
    "settings": { "...": "..." },
    "assessment": { "...": "..." },
    "messages": [
      {
        "role": "user",
        "content": "I have a headache",
        "id": "68d9a84d-711c-4a55-a9dd-da6363e58e38",
        "turn_id": "a1b2c3d4-0000-0000-0000-000000000000"
      },
      {
        "role": "assistant",
        "id": "6f605890-ff28-4b6d-a17f-d410f9faf558",
        "kind": "follow_up",
        "content_parts": [
          { "type": "text", "text": "How long have you had it?" }
        ],
        "turn_id": "a1b2c3d4-0000-0000-0000-000000000000"
      }
    ],
    "is_terminated": false
  }
}

conversation.settings.updated

Acknowledges conversation.settings.update with the effective settings after the merge.

JSON
{
  "type": "conversation.settings.updated",
  "settings": {
    "language": "pl",
    "channel": "voice",
    "ui_widgets_enabled": false,
    "opening_message_enabled": false,
    "risk_factors_enabled": true,
    "geographic_risk_factors_enabled": false,
    "handoff_policies": [],
    "max_message_length": 1500
  }
}

ping

Keep-alive heartbeat sent every 30 seconds. Reply with pong.

JSON
{ "type": "ping" }

This heartbeat is not enforced: a missing pong does not close the connection, and no close code reports one. Keep replying anyway — the exchange is what tells you the socket is still live, and enforcement may be added.

What actually drops a dead connection is the protocol-level ping/pong of the underlying WebSocket layer, which is separate from these frames. Most client libraries answer protocol pings automatically; leave that behaviour enabled, or the server will close a connection that looks healthy to your application.

Conversation flow

An end-to-end session: handshake, one or more non-terminal turns, a terminal turn that closes with conversation.completed, then close.

Client                                                     Server
  │                                                           │
  ├─ conversation.create ────────────────────────────────────▶│
  │◀─────────────────────────────────── conversation.created ─┤
  │                                                           │
  ├─ conversation.user_message.create ───────────────────────▶│
  │◀────────────────────── conversation.user_message.created ─┤
  │◀─────────────────── conversation.assistant_message.delta ─┤ (×N)
  │◀─────────────── conversation.assistant_message.ui_widget ─┤ (×0..N, transient)
  │◀─────────────── conversation.assistant_message.completed ─┤
  │   (delta … completed repeat per assistant message)        │
  │◀──────────────────────── conversation.assessment.updated ─┤
  │◀──────────────────────── conversation.handoff.requested ──┤ (optional)
  │◀──────────────────────────── conversation.turn.completed ─┤
  │                                                           │
  │   (repeat for each non-terminal turn)                     │
  │   (ping every 30 s, pong reply, while connected)          │
  │                                                           │
  ├─ conversation.user_message.create ───────────────────────▶│ ← final turn
  │◀────────────────────── conversation.user_message.created ─┤
  │◀─────────────────── conversation.assistant_message.delta ─┤ (×N)
  │◀─────────────── conversation.assistant_message.completed ─┤
  │◀──────────────────────── conversation.assessment.updated ─┤
  │◀───────────────────────────────── conversation.completed ─┤
  │◀──────────────────────────── conversation.turn.completed ─┤
  │                                                           │
  ├─ [close 1000] ───────────────────────────────────────────▶│

For a conversation.resume first frame, swap conversation.create / conversation.created for conversation.resume / conversation.resumed; the rest is identical.

Client rules

Widgets may arrive out of band. When ui_widgets_enabled is true, widget events may arrive before, between, or after text deltas for the same assistant_message.id — for example a recommendation triage card before the intro text. The completed event always carries the final ordered content_parts.

Widget events are transient and not replayed on reconnect. A client reconnecting mid-turn should ignore transient events until the next terminal turn event, then reconcile from the snapshot and the completed messages.

A user message sent mid-turn preempts the active turn. The server accepts it, ends the running turn with conversation.turn.interrupted, and starts a new one. turn_already_active never occurs on this transport, so send barge-in immediately rather than gating on the in-flight turn — that gate is an HTTP concern.

A turn that does not reach a terminal recommendation may end with conversation.turn.interrupted or conversation.turn.failed instead of conversation.turn.completed.

Error event

The shared error event delivers recoverable protocol or processing errors. The connection stays open after error unless the server also closes the socket. A frame the server cannot accept after the handshake is reported this way and the socket stays open — the 1007 close applies to the first frame only.

JSON
{ "type": "error", "error": { "code": "message_too_long", "message": "..." } }

The codes most commonly emitted on this transport are invalid_payload, invalid_message_type, conversation_not_found, unknown_evidence, message_too_long, rate_limit_exceeded, and turn_failed. The full list is in Error codes.

Close codes

CodeMeaning
1000Normal closure.
1001Server is going away (shutdown in progress).
1007Invalid payload — the first frame was not a valid handshake event.
1008Policy violation, including failed authentication and connection-time rate-limit rejection.
1011Internal server error.

These are the only codes the server sends. A close outside this list came from the WebSocket layer or the network, not from Conversational Triage.

Was this page helpful?