Skip to content

Streaming

Streaming is optional. A basic agent can send one completed message with POST /v1/conversations/{conversationId}/messages.

Agents that stream use the same public HTTP model: one authenticated POST, with the response upgraded to server-sent events when stream=true and Accept: text/event-stream are present. Agent backends do not need a websocket.

POST /v1/conversations/cnv_01JZ8D9H3J7K4M2N1P6Q8R0S2T/messages?stream=true
Authorization: Bearer relay_live_your_api_key
Accept: text/event-stream
Content-Type: application/json
Idempotency-Key: stream-01JZ8J0
{
"schema": "relay.agent-send.v1",
"content": [
{
"type": "text",
"text": "",
"streaming": true
}
],
"fallbackText": ""
}

Relay creates the message, streams patch events, and fans those events out to the iOS app over Relay’s internal realtime channel.

event: message.started
data: {"schema":"relay.event.v1","eventId":"evt_1","conversationId":"conv_trip","type":"message.started","seq":1,"sentAt":"2026-07-06T16:30:00Z","message":{"schema":"relay.message.v1","id":"msg_stream_1","conversationId":"conv_trip","sender":{"kind":"agent","id":"agent_travel","displayName":"Travel"},"role":"agent","direction":"inbound","createdAt":"2026-07-06T16:30:00Z","revision":1,"status":"sending","fallbackText":"","content":[],"agent":{"runId":"run_stream_1","status":"thinking"}}}
event: message.patch
data: {"schema":"relay.event.v1","eventId":"evt_2","conversationId":"conv_trip","type":"message.patch","seq":2,"sentAt":"2026-07-06T16:30:00Z","messageId":"msg_stream_1","revision":2,"patch":{"op":"addBlock","index":0,"block":{"id":"blk_answer","type":"text","text":"","streaming":true}}}
event: message.patch
data: {"schema":"relay.event.v1","eventId":"evt_3","conversationId":"conv_trip","type":"message.patch","seq":3,"sentAt":"2026-07-06T16:30:01Z","messageId":"msg_stream_1","revision":3,"patch":{"op":"appendText","blockId":"blk_answer","path":"/text","value":"I found two flights "}}
event: message.patch
data: {"schema":"relay.event.v1","eventId":"evt_4","conversationId":"conv_trip","type":"message.patch","seq":4,"sentAt":"2026-07-06T16:30:01Z","messageId":"msg_stream_1","revision":4,"patch":{"op":"appendText","blockId":"blk_answer","path":"/text","value":"that fit your constraints."}}
event: message.patch
data: {"schema":"relay.event.v1","eventId":"evt_5","conversationId":"conv_trip","type":"message.patch","seq":5,"sentAt":"2026-07-06T16:30:02Z","messageId":"msg_stream_1","revision":5,"patch":{"op":"addBlock","index":1,"block":{"id":"blk_options","type":"actions","layout":"quick_replies","actions":[{"actionId":"act_show_options","label":"Show options","style":"primary","behavior":{"type":"postback","value":"show_flight_options"}}]}}}
event: message.completed
data: {"schema":"relay.event.v1","eventId":"evt_6","conversationId":"conv_trip","type":"message.completed","seq":6,"sentAt":"2026-07-06T16:30:02Z","messageId":"msg_stream_1","revision":6}
  • message.started includes a valid placeholder message.
  • message.patch events are ordered by seq.
  • revision increases on each message mutation.
  • Text deltas use appendText against /text, /markdown, or /transcript/text.
  • Rich blocks can be added empty and filled progressively.
  • message.completed marks streaming blocks complete.
  • message.failed leaves the partial message visible with a retry affordance.

The non-streaming path always remains first-class:

Terminal window
curl -sS "https://api.relay.app/v1/conversations/${RELAY_CONVERSATION_ID}/messages" \
-H "Authorization: Bearer ${RELAY_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: non-stream-$(date +%s)" \
-d '{
"content": [{ "type": "text", "text": "Here is the completed answer." }],
"fallbackText": "Here is the completed answer."
}'