Stream an agent chat

October 16, 2026 (November 1, 2026)

Table of contents

  1. Request Headers
  2. Query Parameters
  3. Events
  4. Responses
  5. Model
  6. Examples
  7. Try It

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 a data: line with one JSON object. Most events also have an id: line.
  • A new stream first sends a chat event, 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 a replayed event. Everything after replayed is 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 id you received, in the Last-Event-ID header or as ?after=. The stream skips what you already have and goes on live. After a media event id it replays that turn from its start, so de-duplicate messages by id.
  • A keepalive event comes every 15 seconds. When nothing happens for 10 minutes and no video is still being made, an idle event comes and the stream closes. One connection lasts at most 60 minutes, then a reconnect event comes and the stream closes. Reconnect with Last-Event-ID when 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

Request Headers
Authorization: Bearer {API token}
Last-Event-ID: 2:847ad05d-6709-45a0-b75d-b15ea1f5efba   # optional, resume after this id
  • API token is required, see Setup useapi.net for details.
  • Last-Event-ID is optional, the last id you received. The stream sends what came after it.
Query Parameters
  • after is optional, the same as Last-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 approve sent from another window while this stream was open.

  • 400 Bad Request

    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
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "useapi.net ⁝ Unauthorized",
      "code": 401
    }
    
  • 403 Forbidden

    The chatId belongs to another useapi.net user.

    {
      "error": "This chat belongs to another user",
      "code": 403
    }
    
  • 404 Not Found

    The chat was deleted, or its account is not added.

    {
      "error": "Chat not found: it was deleted or never existed",
      "code": 404
    }
    
  • 503 Service Unavailable

    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/email and 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 -N flag 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.