Stream an agent chat
October 16, 2026 (November 1, 2026)
Table of contents
Follow a chat live as Server-Sent Events (SSE): the agent’s messages as each one completes, its requests to spend credits, the end of each turn, and every image and video it makes, including videos that finish after the turn. One stream covers the whole chat, across every turn, until it closes. Start a chat with POST /agent/chats and send more with POST /agent/chats/chatId/messages.
Read the stream with fetch() or any HTTP client that can send the Authorization header. The browser’s EventSource cannot send it.
- Each event has an
event:line with its name and adata:line with one JSON object. Most events also have anid:line. - A new stream first sends a
chatevent, then replays the chat’s history from Google (the agent’s messages and approval requests of every turn, and the media of the last two turns), then areplayedevent. Everything afterreplayedis live. - The stream does not repeat your own messages. They are in GET /agent/chats/
chatId. - To resume after a disconnect, reconnect with the last
idyou received, in theLast-Event-IDheader or as?after=. The stream skips what you already have and goes on live. After amediaevent id it replays that turn from its start, so de-duplicate messages byid. - A
keepaliveevent comes every 15 seconds. When nothing happens for 10 minutes and no video is still being made, anidleevent comes and the stream closes. One connection lasts at most 60 minutes, then areconnectevent comes and the stream closes. Reconnect withLast-Event-IDwhen you want live updates again. - A turn keeps running when you close the stream. Nothing is lost: reconnect, or read the chat with GET.
A stream that fails shows in your usage stats (bot=google-flow) as model agent-stream, status failed. Streams that open and run normally do not appear there.
https://api.useapi.net/v1/google-flow/agent/chats/
chatId/stream?after=after
chatIdis required, URL-encoded in the path (it contains:and@), from POST /agent/chats or GET /agent/chats.
Request Headers
Authorization: Bearer {API token}
Last-Event-ID: 2:847ad05d-6709-45a0-b75d-b15ea1f5efba # optional, resume after this id
API tokenis required, see Setup useapi.net for details.Last-Event-IDis optional, the lastidyou received. The stream sends what came after it.
Query Parameters
afteris optional, the same asLast-Event-ID, for clients that cannot set headers. The header wins when both are sent.
Events
| Event | id | Data | Meaning |
|---|---|---|---|
chat | chatId, title, status, turns | The stream is open. Always first. status is the chat’s state, see GET /agent/chats/chatId. | |
message | <turn>:<messageId> | id, turn, items | One complete message of the agent: its text, thinking, tool calls and tool results. See Agent items. |
approval | <turn>:approval | turn, cost, images, videos, total | The agent asks before it spends cost Flow credits on images images and videos videos. Answer with POST /agent/chats/chatId/approval. |
turn | <turn>:end | turn, status, error | A turn ended while the stream was open: completed, waiting_approval or failed with an error, see Turn errors. |
media | <turn>:m:<mediaId> | the media object | An image or video the agent made, or a change of its status: queued, then completed with its url, or failed. See Media. |
replayed | turns | The history replay is done. Everything after it is live. | |
keepalive | at | Sent every 15 seconds. | |
idle | message | Nothing happened for 10 minutes. The stream closes. | |
reconnect | message | The connection reached 60 minutes. The stream closes: reconnect with Last-Event-ID. | |
error | code, message | The stream failed. Reconnect with Last-Event-ID. |
Responses
-
200 OK —
Content-Type: text/event-stream. A chat that asked for a video, approved it, and waited for it (some messages and fields left out, text shortened):event: chat data: {"chatId":"c-u12345-email:[email protected]:f166200a-ea22-461a-bc68-47ff6d3ee500-chat:2a0f1973-d554-4fbc-aa6b-0c372edf182e-bot:google-flow","status":"running","turns":0} event: replayed data: {"turns":0} id: 1:30387708-2905-484e-888c-967812b9f5d4 event: message data: {"id":"1:30387708-2905-484e-888c-967812b9f5d4","turn":1,"items":[{"kind":"text","text":"I'm going to generate a video of a paper boat floating on a calm pond using Veo 3.1 - Lite with a duration of 8 seconds."},{"kind":"thinking","text":"**Validating Initial Specs**\n\nI'm starting by ensuring \"Veo 3.1 - Lite\" is a valid model…"}]} id: 1:9be1460c-3fb8-4ff1-8f53-3fbce90435ed event: message data: {"id":"1:9be1460c-3fb8-4ff1-8f53-3fbce90435ed","turn":1,"items":[{"kind":"tool_call","tool":"ask_for_permission","args":{"total_cost":10,"num_total":1,"num_images":0,"num_videos":1}}]} id: 1:approval event: approval data: {"turn":1,"cost":10,"images":0,"videos":1,"total":1} id: 1:end event: turn data: {"turn":1,"status":"waiting_approval"} id: 2:b3f9cea0-19f1-4fbb-a207-f3a9cc8c58f9 event: message data: {"id":"2:b3f9cea0-19f1-4fbb-a207-f3a9cc8c58f9","turn":2,"items":[{"kind":"tool_result","tool":"generate_video_from_text","result":{"status":"queued","media_id":"97aa051d-419f-40fd-a200-f5593d997822","display_name":"Paper boat floating on pond","model_key":"veo_3_1_t2v_lite"}}]} id: 2:m:97aa051d-419f-40fd-a200-f5593d997822 event: media data: {"turn":2,"referenceId":"user:12345-email:6a6f...-video:97aa051d-419f-40fd-a200-f5593d997822","mediaId":"97aa051d-419f-40fd-a200-f5593d997822","type":"video","status":"queued","tool":"generate_video_from_text","model":"veo_3_1_t2v_lite","displayName":"Paper boat floating on pond"} id: 2:847ad05d-6709-45a0-b75d-b15ea1f5efba event: message data: {"id":"2:847ad05d-6709-45a0-b75d-b15ea1f5efba","turn":2,"items":[{"kind":"text","text":"Your video of a paper boat on a calm pond has been scheduled. Feel free to check back in a few minutes!","ui":[…]}]} id: 2:end event: turn data: {"turn":2,"status":"completed"} event: keepalive data: {"at":"2026-10-16T22:04:00.064Z"} id: 2:m:97aa051d-419f-40fd-a200-f5593d997822 event: media data: {"turn":2,"referenceId":"user:12345-email:6a6f...-video:97aa051d-419f-40fd-a200-f5593d997822","mediaId":"97aa051d-419f-40fd-a200-f5593d997822","type":"video","status":"completed","tool":"generate_video_from_text","model":"veo_3_1_t2v_lite","displayName":"Paper boat floating on pond","url":"https://flow-content.google/video/97aa051d-419f-40fd-a200-f5593d997822?Expires=...&KeyName=labs-flow-prod-cdn-key&Signature=...","durationSec":8}Turn 2 began with an
approvesent from another window while this stream was open. -
The path value is not a
chatId.{ "error": "Path parameter chatId (not-a-chat-id) is not a chat id from POST /agent/chats or GET /agent/chats", "code": 400 } -
Invalid API token.
{ "error": "useapi.net ⁝ Unauthorized", "code": 401 } -
The
chatIdbelongs to another useapi.net user.{ "error": "This chat belongs to another user", "code": 403 } -
The chat was deleted, or its account is not added.
{ "error": "Chat not found: it was deleted or never existed", "code": 404 } -
Google Flow could not be reached, and nothing happened. Retry shortly.
{ "error": "Google Flow is not reachable right now and could not read the chat. Retry", "code": 503 } -
596 Session Error
The account’s Google session is no longer valid. Delete the account with DELETE /accounts/
emailand add it again by following Setup Google Flow.
Model
The data: JSON of each event:
// event: chat
{ chatId: string, title?: string, status: 'running' | 'waiting_approval' | 'failed' | 'idle', turns: number }
// event: message, id <turn>:<messageId>
{ id: string, turn: number, items: AgentItem[] } // AgentItem: see GET /agent/chats/{chatId}
// event: approval, id <turn>:approval
{ turn: number, cost?: number, images?: number, videos?: number, total?: number } // cost in Flow credits
// event: turn, id <turn>:end
{
turn: number
status: 'completed' | 'waiting_approval' | 'failed'
error?: { code: string, message: string, httpStatus?: number } // failed only
}
// event: media, id <turn>:m:<mediaId>
{ turn: number } & Media // Media: see GET /agent/chats/{chatId}
// event: replayed
{ turns: number }
// event: keepalive
{ at: string }
// event: idle, reconnect
{ message: string }
// event: error
{ code: number, message: string }
Errors before the stream opens return JSON, as in the response tabs above.
Examples
-
CHATID="c-u12345-email:[email protected]:f166200a-...-chat:2a0f1973-...-bot:google-flow" curl -N -H "Authorization: Bearer YOUR_API_TOKEN" \ "https://api.useapi.net/v1/google-flow/agent/chats/$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=''))" "$CHATID")/stream"The
-Nflag turns off buffering, so events print as they arrive. -
const token = 'YOUR_API_TOKEN' const chatId = 'chatId from POST /agent/chats' let lastEventId = '' const response = await fetch(`https://api.useapi.net/v1/google-flow/agent/chats/${encodeURIComponent(chatId)}/stream`, { headers: { 'Authorization': `Bearer ${token}`, ...(lastEventId && { 'Last-Event-ID': lastEventId }) } }) const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' for (;;) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const blocks = buffer.split('\n\n') buffer = blocks.pop() for (const block of blocks) { const field = name => block.split('\n').find(line => line.startsWith(`${name}:`))?.slice(name.length + 1).trim() const event = field('event') const data = JSON.parse(field('data') ?? 'null') if (field('id')) lastEventId = field('id') if (event === 'message') for (const item of data.items) if (item.kind === 'text' && item.text) console.log('agent:', item.text) if (event === 'approval') console.log(`the agent asks to spend ${data.cost} credits`) if (event === 'media' && data.status === 'completed') console.log(data.type, data.url) if (event === 'turn') console.log(`turn ${data.turn} ${data.status}`) } } -
import json, requests from urllib.parse import quote token = 'YOUR_API_TOKEN' chatId = 'chatId from POST /agent/chats' url = f'https://api.useapi.net/v1/google-flow/agent/chats/{quote(chatId, safe="")}/stream' event, last_event_id = None, None with requests.get(url, headers={'Authorization': f'Bearer {token}'}, stream=True) as r: for line in r.iter_lines(decode_unicode=True): if line.startswith('id:'): last_event_id = line[3:].strip() elif line.startswith('event:'): event = line[6:].strip() elif line.startswith('data:'): data = json.loads(line[5:]) if event == 'message': for item in data['items']: if item['kind'] == 'text' and item['text']: print('agent:', item['text']) elif event == 'approval': print(f"the agent asks to spend {data.get('cost')} credits") elif event == 'media' and data['status'] == 'completed': print(data['type'], data['url']) elif event == 'turn': print(f"turn {data['turn']} {data['status']}")
Try It
The form prints each event as it arrives, shows finished images and videos inline, and shows Approve, Always and Reject buttons on each approval event. Stop closes the stream. The turn keeps running.