Stream a chat

October 12, 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): 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. Its event field names the event, see Events.
  • Some events also carry an id: line. An id appears only when everything up to it is complete.
  • To resume after a disconnect, reconnect with the last id you received, in the Last-Event-ID header or as ?after=. After idle (10 quiet minutes) or reconnect (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/chatId with before.
  • A keepalive event comes every 15 seconds. When nothing happens for 10 minutes, an idle event comes and the stream closes. One connection lasts at most 60 minutes: a reconnect event 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

Request Headers
Authorization: Bearer {API token}
Last-Event-ID: 1266   # 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.
    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, or after is not a number.

    {
      "error": "Path parameter chatId (not-a-chat-id) is not a valid Muse chat id",
      "code": 400
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "useapi.net ⁝ Unauthorized",
      "code": 401
    }
    
  • 403 Forbidden — the chatId was 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 -N flag 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.