Stream a chat
October 12, 2026
Table of contents
Follow a chat live as Server-Sent Events (SSE): your messages, the agent’s text as it is written, the files it makes, and the end of each answer. One stream covers the whole chat, across every answer, until you close it. Start a chat with POST /chats and send more with POST /chats/chatId/messages. The Chat Streaming Guide has the full event reference and complete readers in JavaScript and Python.
Read the stream with fetch() or any HTTP client that can send the Authorization header. The browser’s EventSource cannot send it.
- Each event is a
data:line with one JSON object. Itseventfield names the event, see Events. - Some events also carry an
id:line. Anidappears only when everything up to it is complete. - To resume after a disconnect, reconnect with the last
idyou received, in theLast-Event-IDheader or as?after=. Afteridle(10 quiet minutes) orreconnect(60 minutes), reconnect the same way if you still want live updates, for example while the chat is on screen, or else when you next need it. Either way the stream resumes exactly where it left off: messages you missed are replayed, with no history reload, and then it goes on live. - A fresh stream (no
Last-Event-ID) replays the newest 1000 chat events. Older messages are in GET /chats/chatIdwithbefore. - A
keepaliveevent comes every 15 seconds. When nothing happens for 10 minutes, anidleevent comes and the stream closes. One connection lasts at most 60 minutes: areconnectevent comes, and the stream closes. - Each account can have about 50 streams open at the same time. More answer
429. The count is approximate: a connection you close is noticed within about a minute, so a quick burst of reconnects can briefly count more streams than are really open. Keep one stream per chat. GET /chats/streams shows the count.
Every stream you open shows in your usage stats (bot=muse) as model muse-stream, with status opened, refused or failed. Use it to see how often your app opens streams: many opens a minute usually means a reconnect loop.
https://api.useapi.net/v1/muse/chats/
chatId/stream?after=after
chatIdis URL-encoded in the path (it contains:and@), from POST /chats or GET /chats.
Request Headers
Authorization: Bearer {API token}
Last-Event-ID: 1266 # 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.
Default:0, the newest 1000 chat events, then live.
Events
| Event | Fields | Meaning |
|---|---|---|
initialized | chatId, email, after | The stream is open. Always first. |
message | messageId, text, files | A message you sent, with its media files as asset ids and other files as workspace paths. Also a message sent while Muse was answering. |
activity | text | What the agent is doing, for example is working, Generating image. |
text | messageId, text, full | A piece of the agent’s message. Join the pieces with the same messageId. With full: true the event holds the whole message text and replaces any earlier pieces of that message. A link the agent presents on its own, such as a share link, arrives as a message whose whole text is the link (full: true). |
message.done | messageId | The agent’s message is complete. |
widget | messageId, widgetId, kind, text, fallbackText, options, html, data | An interactive card in the agent’s message: quick-reply options, an html card to show only in a sandboxed iframe, or other data. Show fallbackText for a kind you can’t render. See Answer a widget. A widget can also come on its own, with no text in its message. |
file | messageId, assetId, type, mime, bytes, origin | A file the agent made: origin is generated for an image or video Muse generated, made for a file the agent made with its own tools, such as an edited video. It comes once per answer, and again under a later answer that shows the same file: de-duplicate by assetId if you keep a file list. Download it with GET /assets/assetId. |
status | status | The chat’s state from Muse, for example running, pending_user_confirmation, user_confirmed or user_denied, completed, interrupted. pending_user_confirmation means this chat waits for an answer to one of its own approval requests, and user_confirmed or user_denied follows your answer. A chat never shows waiting for another chat’s request. |
approval | approvalId, title, question, details, options, expires | The agent asks for permission before an action. The answer pauses (status pending_user_confirmation) until you answer with POST /chats/chatId/approvals/approvalId, or the request expires. |
approval.resolved | approvalId, status, decision | One of this chat’s requests ended: approved, denied or timed out. Sent once per request. |
turn.done | status, files | The answer ended: completed or interrupted (cancelled). files lists every file of this answer. |
error | code, message | The connection to Muse broke. Reconnect with Last-Event-ID. |
keepalive | 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. |
Responses
-
200 OK —
Content-Type: text/event-stream. A chat that asked for an image (text pieces shortened):data: {"event":"initialized","chatId":"c-u12345-email:[email protected]:272b73f4-6fcb-42c2-9ec7-75cc0c020940-bot:muse","email":"[email protected]","after":0} data: {"event":"status","status":"running"} id: 1270 data: {"event":"message","messageId":"3a1a5487-51a4-42a6-9ae1-b7c6b3ea247f","text":"Create one small image of a red apple on a white table.","files":[]} data: {"event":"activity","text":"Generating image"} data: {"event":"text","messageId":"assistant-msg-92cffe58-3184-44c6-a1ee-cb620081f72a","text":"Here it is."} data: {"event":"file","messageId":"51657274-51ff-4669-9e98-6786d03f206e","assetId":"user:12345-email:6578616d706c6540676d61696c2e636f6d-image:media-generation-create-one-small-image-of-a-re-0-1b5617eb-685d-445f-a289-fdc9a792f696.webp","type":"image","mime":"image/webp","bytes":186402,"origin":"generated"} id: 1272 data: {"event":"message.done","messageId":"assistant-msg-92cffe58-3184-44c6-a1ee-cb620081f72a"} data: {"event":"status","status":"completed"} data: {"event":"turn.done","status":"completed","files":[{"assetId":"user:12345-email:6578616d706c6540676d61696c2e636f6d-image:media-generation-create-one-small-image-of-a-re-0-1b5617eb-685d-445f-a289-fdc9a792f696.webp","type":"image","mime":"image/webp","bytes":186402}]} data: {"event":"keepalive"} -
400 Bad Request — the path value is not a Muse
chatId, orafteris not a number.{ "error": "Path parameter chatId (not-a-chat-id) is not a valid Muse chat id", "code": 400 } -
Invalid API token.
{ "error": "useapi.net ⁝ Unauthorized", "code": 401 } -
403 Forbidden — the
chatIdwas issued to a different API token.{ "error": "This chat id does not belong to this API token", "code": 403 } -
404 Not Found — the chat was deleted, or the account that holds it is not added.
{ "error": "Chat not found", "code": 404 } -
429 Too Many Requests — the account has about 50 streams open. Close the ones you no longer read, and keep one stream per chat. The response carries
Retry-After: 60.{ "error": "Account [email protected] has about 50 open chat streams. Close some (a closed connection is noticed within about a minute), or keep one stream per chat", "code": 429 } -
503 Service Unavailable — Muse could not be reached. Retry shortly.
{ "error": "Could not connect to the Muse workspace of account [email protected] (…), please retry shortly", "code": 503 } -
596 Account Error
The account needs your attention, and it is paused until you add it again. You receive an email when this happens. There are two causes.
Meta asks for a security check on the account. Sign in once at muse.ai in a browser, complete the check, then add the account again with POST /accounts using the same email and password. The API keeps using its session, so no new sign-in is needed.
{ "error": "Account [email protected]: Meta asks for a security check on account [email protected] (\"This account must complete a security checkpoint on the web\"). Sign in once at https://muse.ai in a browser, complete the check, then add the account again with POST /accounts", "code": 596 }Or Muse refused to sign the account in again when its session needed renewing, most often because the password was changed. Add the account again with the current password.
{ "error": "Account [email protected]: Muse refused to sign in account [email protected] (…). Update its password with POST /accounts, or sign in at https://muse.ai to clear the block, then re-add it", "code": 596 }
Model
Each data: line holds one of these objects:
type StreamEvent = // TypeScript
| { event: 'initialized', chatId: string, email: string, after: number }
| { event: 'message', messageId: string, text: string, files: string[] } // your message: media as asset ids, other files as workspace paths
| { event: 'activity', text: string } // e.g. "is working", "Generating image"
| { event: 'text', messageId: string, text: string, full?: true } // full: the whole text, replaces earlier pieces
| { event: 'message.done', messageId: string }
| { event: 'widget', messageId?: string, widgetId: string, // an interactive card in the agent's message
kind: string, text: string | null, fallbackText: string | null, // show fallbackText for a kind you can't render
options?: string[], // kind option: quick replies, answer with POST /chats/{chatId}/widgets/{widgetId}
html?: string, // kind html: a card to show only in a sandboxed iframe
data?: Record<string, unknown> } // any other kind: Muse's widget data as it is
| { event: 'file', messageId?: string, assetId: string,
type: 'image' | 'video' | 'audio', mime: string, bytes: number,
origin: 'generated' | 'made' } // made: by the agent's own tools. Once per answer: a later answer may send the same file again
| { event: 'status', status: string } // e.g. running, pending_user_confirmation (this chat waits for its own approval), user_confirmed / user_denied (after your answer), completed
| { event: 'approval' // the agent asks for permission
approvalId: string
title: string | null // e.g. "Share info with deb.nodesource.com"
question: string | null // e.g. "Allow Muse to share information with deb.nodesource.com?"
details: { label: string, value: string }[] // e.g. Host, Grants, Method
options: { decision: string, label: string }[] // e.g. allow_once "Allow once", deny "Deny"
expires: string | null // ISO 8601, unanswered requests time out then
}
| { event: 'approval.resolved', approvalId: string, // once, when one of this chat's requests ends
status: string, decision: string | null } // e.g. approved / allow_once, denied / deny, timed_out
| { event: 'turn.done', status: 'completed' | 'interrupted',
files: { assetId: string, type: 'image' | 'video' | 'audio', mime: string, bytes: number }[] }
| { event: 'error', code: number, message: string } // reconnect with Last-Event-ID
| { event: 'keepalive' } // every 15 seconds
| { event: 'idle', message: string } // 10 quiet minutes, the stream closes
| { event: 'reconnect', message: string } // 60 minutes reached, the stream closes
Errors before the stream opens return { error, code } as JSON.
Examples
-
CHATID="c-u12345-email:[email protected]:272b73f4-6fcb-42c2-9ec7-75cc0c020940-bot:muse" curl -N -H "Authorization: Bearer …" \ "https://api.useapi.net/v1/muse/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 = "API token"; const chatId = "chatId from POST /chats"; const response = await fetch(`https://api.useapi.net/v1/muse/chats/${encodeURIComponent(chatId)}/stream`, { headers: { "Authorization": `Bearer ${token}` }, }); 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 data = block.split("\n").find(line => line.startsWith("data:")); if (!data) continue; const e = JSON.parse(data.slice(5)); if (e.event === "text") process.stdout.write(e.text); if (e.event === "file") console.log("\nfile", e.assetId); if (e.event === "turn.done") console.log("\nanswer done:", e.status); } } -
import json, requests from urllib.parse import quote token = "API token" chatId = "chatId from POST /chats" apiUrl = f"https://api.useapi.net/v1/muse/chats/{quote(chatId, safe='')}/stream" headers = {"Authorization": f"Bearer {token}"} with requests.get(apiUrl, headers=headers, stream=True) as response: for line in response.iter_lines(decode_unicode=True): if not line or not line.startswith("data:"): continue e = json.loads(line[5:]) if e["event"] == "text": print(e["text"], end="", flush=True) elif e["event"] == "file": print("\nfile", e["assetId"]) elif e["event"] == "turn.done": print("\nanswer done:", e["status"])
Try It
The form prints each event as it arrives, shows images and videos inline, and shows a button for each option of an approval event. Stop closes the stream.