Conversations, messages and events
List and create conversations, send messages and actions, read the event log as JSON or as a server-sent event stream.
All endpoints take Authorization: Bearer kst_…. A conversation is only visible to the end user who owns it; others get 404.
Conversation object
{ "id": "conv_…", "agentId": "agt_…", "title": "Flight to Osaka", "status": "open",
"createdAt": "…", "updatedAt": "…", "lastSeq": 12 }
status is open, handoff or closed.
List and create
GET /v1/conversations → 200 { "conversations": [ … ] } // latest 50 for this user and agent
POST /v1/conversations { "agentId"?, "title"? } → 201 { "conversation": { … } } // emits conversation.created and the greeting
GET /v1/conversations/:id → 200 { "conversation": { … } } // lastSeq is live
POST /v1/conversations/:id/close → 200 { "ok": true }
Messages
GET /v1/conversations/:id/messages?limit=50 → 200 { "messages": [ { "role": "user", "text": "…", "value": null } ] }
POST /v1/conversations/:id/messages
{ "text": "Where is my order ORD-2201?", "clientId": "c_01J8CLIENT0002" }
→ 202 { "accepted": true }
clientIdis required and deduplicates retries; a repeat is a no-op.value(any JSON) may replace or accompanytextfor structured turns such as{ "intent": "select_flight", "flightId": "NH873" }.- The reply arrives as events (
agent.typing,message.created,message.delta,tool.*,ui.render,message.completed), never in the POST response.
Actions
POST /v1/conversations/:id/actions
{ "surfaceId": "sfc_…", "componentId": "b2", "actionId": "book",
"value": { "intent": "book", "hotelId": "h_1" }, "clientId": "c_01J8CLIENT0003" }
→ 202 { "accepted": true }
For form submissions value is { "formId": "f_callback", "values": { … } }; for tool confirmations { "toolCallId": "call_…", "approve": true }.
Events as JSON
GET /v1/conversations/:id/events?after=41&limit=200
→ 200 { "events": [ { "id": "evt_…", "seq": 42, "type": "message.delta", "ts": "…", "conversationId": "conv_…", "turnId": "trn_…", "data": { … } } ],
"lastSeq": 57 }
limit is capped at 1000. Poll with the last seq you saw.
Events as a stream (SSE)
GET /v1/conversations/:id/events?after=41&stream=1
Accept: text/event-stream
event: ready
data: {"seq":41,"conversationId":"conv_…"}
id: 42
event: event
data: {"id":"evt_…","seq":42,"type":"message.delta",…}
: ping
The stream replays events after after, then stays live with a keepalive comment every 20 seconds. Errors arrive as event: error with a frame body. The Flutter SDK uses this when WebSockets are blocked.