Documentation
Conversational Triage API
Build your solution
Streaming turns (NDJSON)

Streaming turns (NDJSON)

POST /conversations/{conversation_id}/turns/stream

Creates one turn and streams its events as they happen. The request body is identical to POST /turns; the response is 200 OK with media type application/x-ndjson, one JSON event per line.

This is the canonical live HTTP transport — use it for chat surfaces that render text as it is generated, without the connection management a WebSocket requires.

cURL
curl -N "https://api.infermedica.com/api/ct/v2/conversations/68d9a84d-711c-4a55-a9dd-da6363e58e38/turns/stream" \
  -X "POST" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "user_message": { "content": "I have a headache" } }'

Events emitted

Each line is one event from the shared streaming events set. The subset this endpoint emits:

EventMeaning
conversation.user_message.createdThe message was accepted and a turn started.
conversation.assistant_message.deltaIncremental text for an in-progress assistant message.
conversation.assistant_message.ui_widgetTransient widget; only when ui_widgets_enabled is true.
conversation.assistant_message.completedFinal form of one assistant message.
conversation.assessment.updatedFull assessment snapshot — replace your local copy.
conversation.handoff.requestedA handoff was recorded this turn.
conversation.terminatedThe conversation can no longer accept messages.
conversation.completedThe conversation reached a terminal recommendation.
conversation.turn.completedTerminal event for a successful turn.
conversation.turn.interruptedTerminal event when a newer input superseded the turn.
conversation.turn.failedTerminal event when the turn failed.
errorRecoverable protocol or processing error.

The WebSocket-only events (conversation.created, conversation.resumed, conversation.settings.updated, ping) never appear here.

Example: a successful non-terminal turn

Widgets disabled:

NDJSON
{"type":"conversation.user_message.created","user_message":{"id":"68d9a84d-711c-4a55-a9dd-da6363e58e38","content":"I have a headache"}}
{"type":"conversation.assistant_message.delta","assistant_message":{"id":"6f605890-ff28-4b6d-a17f-d410f9faf558","turn_id":"a1b2c3d4-0000-0000-0000-000000000000","kind":"follow_up","can_interrupt":true,"delta":"How long have you had it?"}}
{"type":"conversation.assistant_message.completed","assistant_message":{"id":"6f605890-ff28-4b6d-a17f-d410f9faf558","turn_id":"a1b2c3d4-0000-0000-0000-000000000000","kind":"follow_up","can_interrupt":true,"content_parts":[{"type":"text","text":"How long have you had it?"}]}}
{"type":"conversation.assessment.updated","assessment":{"age":{"value":35,"unit":"year"},"sex":"female","relationship":"self","evidence":[{"id":"s_21","name":"Headache","common_name":"Headache","state":"present","origin":"asked","seriousness":null,"parent_id":null}],"unmatched_evidence":[],"conditions":[],"recommendation":null}}
{"type":"conversation.turn.completed","turn_id":"a1b2c3d4-0000-0000-0000-000000000000"}

With ui_widgets_enabled set to true, a transient widget can arrive before the text completes:

NDJSON
{"type":"conversation.assistant_message.ui_widget","assistant_message":{"id":"6f605890-ff28-4b6d-a17f-d410f9faf558","turn_id":"a1b2c3d4-0000-0000-0000-000000000000","widget":{"type":"hints","hints":["Yes","No","Not sure"]}}}
{"type":"conversation.assistant_message.completed","assistant_message":{"id":"6f605890-ff28-4b6d-a17f-d410f9faf558","turn_id":"a1b2c3d4-0000-0000-0000-000000000000","kind":"intro","can_interrupt":true,"content_parts":[{"type":"text","text":"Do you have a fever?"},{"type":"ui_widget","widget":{"type":"hints","hints":["Yes","No","Not sure"]}}]}}

When the stream ends

The stream closes once a terminal turn event — conversation.turn.completed, conversation.turn.interrupted, or conversation.turn.failed — has been emitted for the turn.

Client rules

  • Build messages from content_parts, never from accumulated deltas. Deltas carry text only; widgets never appear in them. A client that concatenates deltas silently drops them.
  • Replace the assessment, don't merge it. conversation.assessment.updated carries the complete snapshot.
  • Treat widget events as progressive rendering only. They are transient and not replayed; content_parts on the completed event is the durable source of truth.
  • Expect exactly one terminal turn event per turncompleted, interrupted, or failed, mutually exclusive.

The canonical per-turn event order is documented under Successful turn order.

Was this page helpful?