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.
Start a streamed reply
Section titled “Start a streamed reply”POST /v1/conversations/cnv_01JZ8D9H3J7K4M2N1P6Q8R0S2T/messages?stream=trueAuthorization: Bearer relay_live_your_api_keyAccept: text/event-streamContent-Type: application/jsonIdempotency-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.
SSE response
Section titled “SSE response”event: message.starteddata: {"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.patchdata: {"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.patchdata: {"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.patchdata: {"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.patchdata: {"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.completeddata: {"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}Patch rules
Section titled “Patch rules”message.startedincludes a valid placeholder message.message.patchevents are ordered byseq.revisionincreases on each message mutation.- Text deltas use
appendTextagainst/text,/markdown, or/transcript/text. - Rich blocks can be added empty and filled progressively.
message.completedmarks streaming blocks complete.message.failedleaves the partial message visible with a retry affordance.
Non-streaming fallback
Section titled “Non-streaming fallback”The non-streaming path always remains first-class:
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." }'